ood_support 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +150 -0
- data/lib/ood_support.rb +8 -1
- data/lib/ood_support/acl.rb +89 -0
- data/lib/ood_support/acl_entry.rb +76 -0
- data/lib/ood_support/acls/nfs4.rb +262 -0
- data/lib/ood_support/acls/posix.rb +274 -0
- data/lib/ood_support/errors.rb +16 -0
- data/lib/ood_support/group.rb +3 -3
- data/lib/ood_support/user.rb +7 -3
- data/lib/ood_support/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74117abef87ed8fce6977b36e54d99795db90ba2
|
4
|
+
data.tar.gz: f82d7ee819ed4bacece8ea084a3bb472e47105e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61abe8f3a20b7d6a9b6c3094d7b3490459dbf6629efd2559c420981ebb7257f7d617399177319fa84801ec48cd13dc44bb336ea1e50e5a35feeac42b43c13cee
|
7
|
+
data.tar.gz: 173d29d517fc5f9da03cefe598e32a195301eb3752d7f982edf74997f0e2cfbc1d0591f4907a0fe7f091bd2e79aadb597f6dbec20ba7ca2eae2c5f1696958644
|
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -156,6 +156,156 @@ OodSupport::Process.groups_changed?
|
|
156
156
|
#=> false
|
157
157
|
```
|
158
158
|
|
159
|
+
### ACLs
|
160
|
+
|
161
|
+
#### NFSv4 File ACLs
|
162
|
+
|
163
|
+
Allows reading and writing of NFSv4 file ACL permissions.
|
164
|
+
|
165
|
+
To access a file's ACL:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
# Get file ACL
|
169
|
+
acl = OodSupport::ACLs::Nfs4ACL.get_facl(path: "/path/to/file")
|
170
|
+
|
171
|
+
# Check if user has read access to file
|
172
|
+
acl.allow?(principle: OodSupport::User.new("user1"), permission: :r)
|
173
|
+
#=> true
|
174
|
+
|
175
|
+
# Check if group has write access to file
|
176
|
+
# NB: A user of this group may *actually* have access to write to this file
|
177
|
+
acl.allow?(principle: OodSupport::Group.new("group1"), permission: :w)
|
178
|
+
#=> false
|
179
|
+
```
|
180
|
+
|
181
|
+
To add an ACL permission to a file:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
# Create a new ACL entry
|
185
|
+
entry = OodSupport::ACLs::Nfs4Entry.new(type: :A, flags: [], principle: "user2", domain: "osc.edu", permissions: [:r, :w])
|
186
|
+
|
187
|
+
# or you can pass it a properly formatted string...
|
188
|
+
entry = OodSupport::ACLs::Nfs4Entry.parse("A::user2@osc.edu:rw")
|
189
|
+
|
190
|
+
# Add this entry to the file ACLs
|
191
|
+
acl = OodSupport::ACLs::Nfs4ACL.add_facl(path: "/path/to/file", entry: entry)
|
192
|
+
|
193
|
+
# Check that this added entry changes access
|
194
|
+
acl.allow?(principle: OodSupport::User.new("user2"), permission: :r)
|
195
|
+
#=> true
|
196
|
+
```
|
197
|
+
|
198
|
+
To remove an ACL permission from a file:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
# Get file ACL
|
202
|
+
acl = OodSupport::ACLs::Nfs4ACL.get_facl(path: "/path/to/file")
|
203
|
+
|
204
|
+
# Choose the entry we want to remove from the array of entries
|
205
|
+
entry = acl.entries.first
|
206
|
+
|
207
|
+
# Remove this entry from the file
|
208
|
+
new_acl = OodSupport::ACLs::Nfs4ACL.rem_facl(path: "/path/to/file", entry: entry)
|
209
|
+
|
210
|
+
# Check that this entry removal changes access
|
211
|
+
new_acl.allow?(principle: OodSupport::User.new("user2"), permission: :r)
|
212
|
+
#=> false
|
213
|
+
```
|
214
|
+
|
215
|
+
##### File ACL Methods
|
216
|
+
|
217
|
+
List of class methods on the `Nfs4ACL` object used to access/modify a file's
|
218
|
+
ACL. For all class methods an `Nfs4ACL` object is created and returned.
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
# Get the file/directory ACLs for a given path
|
222
|
+
Nfs4ACL::get_facl(path: p)
|
223
|
+
|
224
|
+
# Add an ACL entry to the given file/directory ACLs
|
225
|
+
Nfs4ACL::add_facl(path: p, entry: e)
|
226
|
+
|
227
|
+
# Remove an ACL entry from the given file/directory ACLs
|
228
|
+
Nfs4ACL::rem_facl(path: p, entry: e)
|
229
|
+
|
230
|
+
# Modify in-place an ACL entry from the given file/directory ACLs
|
231
|
+
Nfs4ACL::mod_facl(path: p, old_entry: e1, new_entry: e2)
|
232
|
+
|
233
|
+
# Set the whole ACL (overwrites original) for a given file/directory
|
234
|
+
Nfs4ACL::set_facl(path: p, acl: a)
|
235
|
+
```
|
236
|
+
|
237
|
+
#### Posix File ACLs
|
238
|
+
|
239
|
+
Allows reading and writing of Posix file ACL permissions.
|
240
|
+
|
241
|
+
To access a file's ACL:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
# Get file ACL
|
245
|
+
acl = OodSupport::ACLs::PosixACL.get_facl path: "/path/to/file"
|
246
|
+
|
247
|
+
# Check if user has read access to file
|
248
|
+
acl.allow?(principle: OodSupport::User.new("user1"), permission: :r)
|
249
|
+
#=> true
|
250
|
+
|
251
|
+
# Check if group has write access to file
|
252
|
+
# NB: A user of this group may *actually* have access to write to this file
|
253
|
+
acl.allow?(principle: OodSupport::Group.new("group1"), permission: :w)
|
254
|
+
#=> false
|
255
|
+
```
|
256
|
+
|
257
|
+
To add an ACL permission to a file:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
# Create a new ACL entry
|
261
|
+
entry = OodSupport::ACLs::PosixEntry.new(flag: :user, principle: "user2", permissions: [:r, :w, :-])
|
262
|
+
|
263
|
+
# or you can pass it a properly formatted string...
|
264
|
+
entry = OodSupport::ACLs::PosixEntry.parse("user:user2:rw-")
|
265
|
+
|
266
|
+
# Add this entry to the file ACLs
|
267
|
+
acl = OodSupport::ACLs::PosixACL.add_facl(path: "/path/to/file", entry: entry)
|
268
|
+
|
269
|
+
# Check that this added entry changes access
|
270
|
+
acl.allow?(principle: OodSupport::User.new("user2"), permission: :r)
|
271
|
+
#=> true
|
272
|
+
```
|
273
|
+
|
274
|
+
To remove an ACL permission from a file:
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
# Get file ACL
|
278
|
+
acl = OodSupport::ACLs::PosixACL.get_facl(path: "/path/to/file")
|
279
|
+
|
280
|
+
# Choose the entry we want to remove from the array of entries
|
281
|
+
entry = acl.entries.detect {|e| e.user_entry? && e.principle == "user2"}
|
282
|
+
|
283
|
+
# Remove this entry from the file
|
284
|
+
new_acl = OodSupport::ACLs::PosixACL.rem_facl(path: "/path/to/file", entry: entry)
|
285
|
+
|
286
|
+
# Check that this entry removal changes access
|
287
|
+
new_acl.allow?(principle: OodSupport::User.new("user2"), permission: :r)
|
288
|
+
#=> false
|
289
|
+
```
|
290
|
+
|
291
|
+
##### File ACL Methods
|
292
|
+
|
293
|
+
List of class methods on the `PosixACL` object used to access/modify a file's
|
294
|
+
ACL. For all class methods an `PosixACL` object is created and returned.
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
# Get the file/directory ACLs for a given path
|
298
|
+
PosixACL::get_facl(path: p)
|
299
|
+
|
300
|
+
# Add an ACL entry to the given file/directory ACLs
|
301
|
+
PosixACL::add_facl(path: p, entry: e)
|
302
|
+
|
303
|
+
# Remove an ACL entry from the given file/directory ACLs
|
304
|
+
PosixACL::rem_facl(path: p, entry: e)
|
305
|
+
|
306
|
+
# Clear all extended ACLs for the given file/directory
|
307
|
+
PosixACL::clear_facl(path: p)
|
308
|
+
```
|
159
309
|
## Contributing
|
160
310
|
|
161
311
|
1. Fork it ( https://github.com/[my-github-username]/ood_support/fork )
|
data/lib/ood_support.rb
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
require 'ood_support/version'
|
2
|
+
require 'ood_support/errors'
|
2
3
|
require 'ood_support/user'
|
3
4
|
require 'ood_support/group'
|
4
5
|
require 'ood_support/process'
|
6
|
+
require 'ood_support/acl'
|
7
|
+
require 'ood_support/acl_entry'
|
5
8
|
|
6
9
|
# The main namespace for ood_support
|
7
10
|
module OodSupport
|
8
|
-
#
|
11
|
+
# A namespace to hold all subclasses of {ACL} and {ACLEntry}
|
12
|
+
module ACLs
|
13
|
+
require 'ood_support/acls/nfs4'
|
14
|
+
require 'ood_support/acls/posix'
|
15
|
+
end
|
9
16
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module OodSupport
|
2
|
+
# A helper object that describes an access control list (ACL) with entries
|
3
|
+
class ACL
|
4
|
+
# The entries of this ACL
|
5
|
+
# @return [Array<ACLEntry>] list of entries
|
6
|
+
attr_reader :entries
|
7
|
+
|
8
|
+
# Whether this ACL defaults to allow, otherwise default deny
|
9
|
+
# @return [Boolean] whether default allow
|
10
|
+
attr_reader :default
|
11
|
+
|
12
|
+
# Generate an ACL by parsing a string along with options
|
13
|
+
# @param acl [#to_s] string describing acl
|
14
|
+
# @param kwargs [Hash] extra arguments defining acl
|
15
|
+
# @return [ACL] acl generated by string and options
|
16
|
+
def self.parse(acl, **kwargs)
|
17
|
+
entries = []
|
18
|
+
acl.to_s.strip.split(/\n|,/).grep(/^[^#]/).each do |entry|
|
19
|
+
entries << entry_class.parse(entry)
|
20
|
+
end
|
21
|
+
new(entries: entries, **kwargs)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param entries [#to_s] list of entries
|
25
|
+
# @param default [Boolean] default allow, otherwise deny
|
26
|
+
def initialize(entries:, default: false)
|
27
|
+
@entries = entries
|
28
|
+
@default = default
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check if queried principle has access to resource
|
32
|
+
# @param principle [String] principle to check against
|
33
|
+
# @return [Boolean] does principle have access?
|
34
|
+
def allow?(principle:)
|
35
|
+
# Check in array order
|
36
|
+
ordered_check(principle: principle)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Convert object to string
|
40
|
+
# @return [String] the string describing this object
|
41
|
+
def to_s
|
42
|
+
entries.join("\n")
|
43
|
+
end
|
44
|
+
|
45
|
+
# Convert object to hash
|
46
|
+
# @return [Hash] the hash describing this object
|
47
|
+
def to_h
|
48
|
+
{ entries: entries, default: default }
|
49
|
+
end
|
50
|
+
|
51
|
+
# The comparison operator
|
52
|
+
# @param other [#to_h] entry to compare against
|
53
|
+
# @return [Boolean] how acls compare
|
54
|
+
def ==(other)
|
55
|
+
to_h == other.to_h
|
56
|
+
end
|
57
|
+
|
58
|
+
# Checks whether two ACL objects are completely identical to each other
|
59
|
+
# @param other [ACL] entry to compare against
|
60
|
+
# @return [Boolean] whether same objects
|
61
|
+
def eql?(other)
|
62
|
+
self.class == other.class && self == other
|
63
|
+
end
|
64
|
+
|
65
|
+
# Generates a hash value for this object
|
66
|
+
# @return [Fixnum] hash value of object
|
67
|
+
def hash
|
68
|
+
[self.class, to_h].hash
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
# Class used to generate an entry
|
73
|
+
def self.entry_class
|
74
|
+
ACLEntry
|
75
|
+
end
|
76
|
+
|
77
|
+
# Check each entry in order from array
|
78
|
+
def ordered_check(**kwargs)
|
79
|
+
entries.each do |entry|
|
80
|
+
if entry.match(**kwargs)
|
81
|
+
# Check if its an allow or deny acl entry (may not be both)
|
82
|
+
return true if entry.is_allow?
|
83
|
+
return false if entry.is_deny?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
return default # default allow or default deny
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module OodSupport
|
2
|
+
# A helper object that defines a generic ACL entry
|
3
|
+
class ACLEntry
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
# The principle of this entry
|
7
|
+
# @return [String] principle of entry
|
8
|
+
attr_reader :principle
|
9
|
+
|
10
|
+
# Generate an entry by parsing a string
|
11
|
+
# @param entry [#to_s] string describing entry
|
12
|
+
# @param kwargs [Hash] extra arguments
|
13
|
+
# @raise [InvalidACLEntry] unable to parse entry string
|
14
|
+
# @return [ACLEntry] entry generated by string
|
15
|
+
def self.parse(entry, **kwargs)
|
16
|
+
new(parse_entry(entry).merge(kwargs))
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param principle [#to_s] principle for this ACL entry
|
20
|
+
def initialize(principle:)
|
21
|
+
@principle = principle.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
# Is this an "allow" ACL entry
|
25
|
+
# @return [Boolean] is this an allow entry
|
26
|
+
def is_allow?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
# Is this a "deny" ACL entry
|
31
|
+
# @return [Boolean] is this a deny entry
|
32
|
+
def is_deny?
|
33
|
+
!is_allow?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Do the requested args match this ACL entry?
|
37
|
+
# @params principle [String] requested principle
|
38
|
+
# @return [Boolean] does this match this entry
|
39
|
+
def match(principle:)
|
40
|
+
self.principle == principle
|
41
|
+
end
|
42
|
+
|
43
|
+
# Convert object to string
|
44
|
+
# @return [String] the string describing this object
|
45
|
+
def to_s
|
46
|
+
principle
|
47
|
+
end
|
48
|
+
|
49
|
+
# The comparison operator
|
50
|
+
# @param other [#to_s] entry to compare against
|
51
|
+
# @return [Boolean] how entries compare
|
52
|
+
def <=>(other)
|
53
|
+
to_s <=> other
|
54
|
+
end
|
55
|
+
|
56
|
+
# Checks whether two ACLEntry objects are completely identical to each
|
57
|
+
# other
|
58
|
+
# @param other [ACLEntry] entry to compare against
|
59
|
+
# @return [Boolean] whether same objects
|
60
|
+
def eql?(other)
|
61
|
+
self.class == other.class && self == other
|
62
|
+
end
|
63
|
+
|
64
|
+
# Generates a hash value for this object
|
65
|
+
# @return [Fixnum] hash value of object
|
66
|
+
def hash
|
67
|
+
[self.class, to_s].hash
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
# Parse an entry string into input parameters
|
72
|
+
def self.parse_entry(entry)
|
73
|
+
{ principle: entry.to_s.strip }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,262 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module OodSupport
|
4
|
+
module ACLs
|
5
|
+
# Object describing an NFSv4 ACL
|
6
|
+
class Nfs4ACL < ACL
|
7
|
+
# The binary used to get the file ACLs
|
8
|
+
GET_FACL_BIN = 'nfs4_getfacl'
|
9
|
+
|
10
|
+
# The binary used to set the file ACLs
|
11
|
+
SET_FACL_BIN = 'nfs4_setfacl'
|
12
|
+
|
13
|
+
# Name of owner for this ACL
|
14
|
+
# @return [String] owner name
|
15
|
+
attr_reader :owner
|
16
|
+
|
17
|
+
# Name of owning group for this ACL
|
18
|
+
# @return [String] group name
|
19
|
+
attr_reader :group
|
20
|
+
|
21
|
+
# Get ACL from file path
|
22
|
+
# @param path [String] path to file or directory
|
23
|
+
# @raise [InvalidPath] file path doesn't exist
|
24
|
+
# @raise [BadExitCode] the command line called exited with non-zero status
|
25
|
+
# @return [Nfs4ACL] acl generated from path
|
26
|
+
def self.get_facl(path:)
|
27
|
+
path = Pathname.new path
|
28
|
+
raise InvalidPath, "invalid path: #{path}" unless path.exist?
|
29
|
+
stat = path.stat
|
30
|
+
acl, err, s = Open3.capture3(GET_FACL_BIN, path.to_s)
|
31
|
+
raise BadExitCode, err unless s.success?
|
32
|
+
parse(acl, owner: User.new(stat.uid), group: Group.new(stat.gid))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Add ACL to file path
|
36
|
+
# @param path [String] path to file or directory
|
37
|
+
# @param entry [Nfs4Entry] entry to add to file
|
38
|
+
# @raise [InvalidPath] file path doesn't exist
|
39
|
+
# @raise [BadExitCode] the command line called exited with non-zero status
|
40
|
+
# @return [Nfs4ACL] new acl of path
|
41
|
+
def self.add_facl(path:, entry:)
|
42
|
+
path = Pathname.new path
|
43
|
+
raise InvalidPath, "invalid path: #{path}" unless path.exist?
|
44
|
+
_, err, s = Open3.capture3(SET_FACL_BIN, '-a', entry.to_s, path.to_s)
|
45
|
+
raise BadExitCode, err unless s.success?
|
46
|
+
get_facl(path: path)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Remove ACL from file path
|
50
|
+
# @param path [String] path to file or directory
|
51
|
+
# @param entry [Nfs4Entry] entry to remove from file
|
52
|
+
# @raise [InvalidPath] file path doesn't exist
|
53
|
+
# @raise [BadExitCode] the command line called exited with non-zero status
|
54
|
+
# @return [Nfs4ACL] new acl of path
|
55
|
+
def self.rem_facl(path:, entry:)
|
56
|
+
path = Pathname.new path
|
57
|
+
raise InvalidPath, "invalid path: #{path}" unless path.exist?
|
58
|
+
_, err, s = Open3.capture3(SET_FACL_BIN, '-x', entry.to_s, path.to_s)
|
59
|
+
raise BadExitCode, err unless s.success?
|
60
|
+
get_facl(path: path)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Modify in-place an entry for file path
|
64
|
+
# @param path [String] path to file or directory
|
65
|
+
# @param old_entry [Nfs4Entry] old entry to modify in-place in file
|
66
|
+
# @param new_entry [Nfs4Entry] new entry to be replaced with
|
67
|
+
# @raise [InvalidPath] file path doesn't exist
|
68
|
+
# @raise [BadExitCode] the command line called exited with non-zero status
|
69
|
+
# @return [Nfs4ACL] new acl of path
|
70
|
+
def self.mod_facl(path:, old_entry:, new_entry:)
|
71
|
+
path = Pathname.new path
|
72
|
+
raise InvalidPath, "invalid path: #{path}" unless path.exist?
|
73
|
+
_, err, s = Open3.capture3(SET_FACL_BIN, '-m', old_entry.to_s, new_entry.to_s, path.to_s)
|
74
|
+
raise BadExitCode, err unless s.success?
|
75
|
+
get_facl(path: path)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Set ACL (overwrites original) for file path
|
79
|
+
# @param path [String] path to file or directory
|
80
|
+
# @param acl [Nfs4ACL] ACL to overwrite original with
|
81
|
+
# @raise [InvalidPath] file path doesn't exist
|
82
|
+
# @raise [BadExitCode] the command line called exited with non-zero status
|
83
|
+
# @return [Nfs4ACL] new acl of path
|
84
|
+
def self.set_facl(path:, acl:)
|
85
|
+
path = Pathname.new path
|
86
|
+
raise InvalidPath, "invalid path: #{path}" unless path.exist?
|
87
|
+
_, err, s = Open3.capture3(SET_FACL_BIN, '-s', acl.to_s, path.to_s)
|
88
|
+
raise BadExitCode, err unless s.success?
|
89
|
+
get_facl(path: path)
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param owner [#to_s] name of owner
|
93
|
+
# @param group [#to_s] name of group
|
94
|
+
# @see ACL#initialize
|
95
|
+
def initialize(owner:, group:, **kwargs)
|
96
|
+
super(kwargs.merge(default: false))
|
97
|
+
@owner = owner.to_s
|
98
|
+
@group = group.to_s
|
99
|
+
end
|
100
|
+
|
101
|
+
# Check if queried principle has access to resource
|
102
|
+
# @param principle [User, Group] principle to check against
|
103
|
+
# @param permission [Symbol] permission to check against
|
104
|
+
# @return [Boolean] does principle have access?
|
105
|
+
def allow?(principle:, permission:)
|
106
|
+
# Check in array order
|
107
|
+
ordered_check(principle: principle, permission: permission, owner: owner, group: group)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Convert object to hash
|
111
|
+
# @return [Hash] the hash describing this object
|
112
|
+
def to_h
|
113
|
+
super.merge owner: owner, group: group
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
# Use Nfs4Entry for entry objects
|
118
|
+
def self.entry_class
|
119
|
+
Nfs4Entry
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Object describing single NFSv4 ACL entry
|
124
|
+
class Nfs4Entry < ACLEntry
|
125
|
+
# Valid types for an ACL entry
|
126
|
+
VALID_TYPE = %i[ A U D L ]
|
127
|
+
|
128
|
+
# Valid flags for an ACL entry
|
129
|
+
VALID_FLAG = %i[ f d p i S F g ]
|
130
|
+
|
131
|
+
# Valid permissions for an ACL entry
|
132
|
+
VALID_PERMISSION = %i[ r w a x d D t T n N c C o y ]
|
133
|
+
|
134
|
+
# Regular expression used when parsing ACL entry string
|
135
|
+
REGEX_PATTERN = %r[^(?<type>[#{VALID_TYPE.join}]):(?<flags>[#{VALID_FLAG.join}]*):(?<principle>\w+)@(?<domain>[\w\.\-]*):(?<permissions>[#{VALID_PERMISSION.join}]+)$]
|
136
|
+
|
137
|
+
# Type of ACL entry
|
138
|
+
# @return [Symbol] type of acl entry
|
139
|
+
attr_reader :type
|
140
|
+
|
141
|
+
# Flags set on ACL entry
|
142
|
+
# @return [Array<Symbol>] flags on acl entry
|
143
|
+
attr_reader :flags
|
144
|
+
|
145
|
+
# Domain of ACL entry
|
146
|
+
# @return [String] domain of acl entry
|
147
|
+
attr_reader :domain
|
148
|
+
|
149
|
+
# Permissions of ACL entry
|
150
|
+
# @return [Array<Symbol>] permissions of acl entry
|
151
|
+
attr_reader :permissions
|
152
|
+
|
153
|
+
# @param type [#to_sym] type of acl entry
|
154
|
+
# @param flags [Array<#to_sym>] list of flags for entry
|
155
|
+
# @param domain [#to_s] domain of principle
|
156
|
+
# @param permissions [Array<#to_sym>] list of permissions for entry
|
157
|
+
# @see ACLEntry#initialize
|
158
|
+
def initialize(type:, flags:, domain:, permissions:, **kwargs)
|
159
|
+
@type = type.to_sym
|
160
|
+
@flags = flags.map(&:to_sym)
|
161
|
+
@domain = domain.to_s
|
162
|
+
@permissions = permissions.map(&:to_sym)
|
163
|
+
super(kwargs)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Is this an "allow" ACL entry
|
167
|
+
# @return [Boolean] is this an allow entry
|
168
|
+
def is_allow?
|
169
|
+
type == :A
|
170
|
+
end
|
171
|
+
|
172
|
+
# Is this a "deny" ACL entry
|
173
|
+
# @return [Boolean] is this a deny entry
|
174
|
+
def is_deny?
|
175
|
+
type == :D
|
176
|
+
end
|
177
|
+
|
178
|
+
# Do the requested args match this ACL entry?
|
179
|
+
# @param principle [User, Group, #to_s] requested principle
|
180
|
+
# @param permission [#to_sym] requested permission
|
181
|
+
# @param owner [String] owner of corresponding ACL
|
182
|
+
# @param group [String] owning group of corresponding ACL
|
183
|
+
# @raise [ArgumentError] principle isn't {User} or {Group} object
|
184
|
+
# @return [Boolean] does this match this entry
|
185
|
+
def match(principle:, permission:, owner:, group:)
|
186
|
+
principle = User.new(principle) if (!principle.is_a?(User) && !principle.is_a?(Group))
|
187
|
+
return false unless has_permission?(permission: permission)
|
188
|
+
# Ignore domain, I don't want or care to check for domain matches
|
189
|
+
p = self.principle
|
190
|
+
p = owner if user_owner_entry?
|
191
|
+
p = group if group_owner_entry?
|
192
|
+
if (principle.is_a?(User) && group_entry?)
|
193
|
+
principle.groups.include?(p)
|
194
|
+
elsif (principle.is_a?(User) && user_entry?) || (principle.is_a?(Group) && group_entry?)
|
195
|
+
principle == p
|
196
|
+
elsif other_entry?
|
197
|
+
true
|
198
|
+
else
|
199
|
+
false
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Is this a user-specific ACL entry
|
204
|
+
# @return [Boolean] is this a user entry
|
205
|
+
def user_entry?
|
206
|
+
!group_entry? && !other_entry?
|
207
|
+
end
|
208
|
+
|
209
|
+
# Is this a group-specific ACL entry
|
210
|
+
# @return [Boolean] is this a group entry
|
211
|
+
def group_entry?
|
212
|
+
flags.include? :g
|
213
|
+
end
|
214
|
+
|
215
|
+
# Is this an other-specific ACL entry
|
216
|
+
# @return [Boolean] is this an other entry
|
217
|
+
def other_entry?
|
218
|
+
principle == "EVERYONE"
|
219
|
+
end
|
220
|
+
|
221
|
+
# Is this the owner ACL entry
|
222
|
+
# @return [Boolean] is this the owner entry
|
223
|
+
def user_owner_entry?
|
224
|
+
user_entry? && principle == "OWNER"
|
225
|
+
end
|
226
|
+
|
227
|
+
# Is this the owning group ACL entry
|
228
|
+
# @return [Boolean] is this the owning group entry
|
229
|
+
def group_owner_entry?
|
230
|
+
group_entry? && principle == "GROUP"
|
231
|
+
end
|
232
|
+
|
233
|
+
# Does this entry have the requested permission
|
234
|
+
# @param permission [#to_sym] the requested permission
|
235
|
+
# @return [Boolean] found this permission
|
236
|
+
def has_permission?(permission:)
|
237
|
+
permissions.include? permission.to_sym
|
238
|
+
end
|
239
|
+
|
240
|
+
# Convert object to string
|
241
|
+
# @return [String] the string describing this object
|
242
|
+
def to_s
|
243
|
+
"#{type}:#{flags.join}:#{principle}@#{domain}:#{permissions.join}"
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
# Parse an entry string into input parameters
|
248
|
+
def self.parse_entry(entry)
|
249
|
+
e = REGEX_PATTERN.match(entry.to_s.strip) do |m|
|
250
|
+
{
|
251
|
+
type: m[:type],
|
252
|
+
flags: m[:flags].chars,
|
253
|
+
principle: m[:principle],
|
254
|
+
domain: m[:domain],
|
255
|
+
permissions: m[:permissions].chars
|
256
|
+
}
|
257
|
+
end
|
258
|
+
e ? e : raise(InvalidACLEntry, "invalid entry: #{entry}")
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module OodSupport
|
4
|
+
module ACLs
|
5
|
+
# Object describing a Posix ACL
|
6
|
+
class PosixACL < ACL
|
7
|
+
# The binary used to get the file ACLs
|
8
|
+
GET_FACL_BIN = 'getfacl'
|
9
|
+
|
10
|
+
# The binary used to set the file ACLs
|
11
|
+
SET_FACL_BIN = 'setfacl'
|
12
|
+
|
13
|
+
# Name of owner for this ACL
|
14
|
+
# @return [String] owner name
|
15
|
+
attr_reader :owner
|
16
|
+
|
17
|
+
# Name of owning group for this ACL
|
18
|
+
# @return [String] group name
|
19
|
+
attr_reader :group
|
20
|
+
|
21
|
+
# Mask set for this ACL
|
22
|
+
# @return [Array<Symbol>] mask for this acl
|
23
|
+
attr_reader :mask
|
24
|
+
|
25
|
+
# Get ACL from file path
|
26
|
+
# @param path [String] path to file or directory
|
27
|
+
# @raise [InvalidPath] file path doesn't exist
|
28
|
+
# @raise [BadExitCode] the command line called exited with non-zero status
|
29
|
+
# @return [PosixACL] acl generated from path
|
30
|
+
def self.get_facl(path:)
|
31
|
+
path = Pathname.new path
|
32
|
+
raise InvalidPath, "invalid path: #{path}" unless path.exist?
|
33
|
+
stat = path.stat
|
34
|
+
acl, err, s = Open3.capture3(GET_FACL_BIN, path.to_s)
|
35
|
+
raise BadExitCode, err unless s.success?
|
36
|
+
parse(acl, owner: User.new(stat.uid), group: Group.new(stat.gid))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add ACL to file path
|
40
|
+
# @param path [String] path to file or directory
|
41
|
+
# @param entry [PosixEntry] entry to add to file
|
42
|
+
# @raise [InvalidPath] file path doesn't exist
|
43
|
+
# @raise [BadExitCode] the command line called exited with non-zero status
|
44
|
+
# @return [PosixACL] new acl of path
|
45
|
+
def self.add_facl(path:, entry:)
|
46
|
+
path = Pathname.new path
|
47
|
+
raise InvalidPath, "invalid path: #{path}" unless path.exist?
|
48
|
+
_, err, s = Open3.capture3(SET_FACL_BIN, '-m', entry.to_s, path.to_s)
|
49
|
+
raise BadExitCode, err unless s.success?
|
50
|
+
get_facl(path: path)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Remove ACL from file path
|
54
|
+
# @param path [String] path to file or directory
|
55
|
+
# @param entry [PosixEntry] entry to remove from file
|
56
|
+
# @raise [InvalidPath] file path doesn't exist
|
57
|
+
# @raise [BadExitCode] the command line called exited with non-zero status
|
58
|
+
# @return [PosixACL] new acl of path
|
59
|
+
def self.rem_facl(path:, entry:)
|
60
|
+
path = Pathname.new path
|
61
|
+
raise InvalidPath, "invalid path: #{path}" unless path.exist?
|
62
|
+
_, err, s = Open3.capture3(SET_FACL_BIN, '-x', entry.to_s(w_perms: false), path.to_s)
|
63
|
+
raise BadExitCode, err unless s.success?
|
64
|
+
get_facl(path: path)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Clear all extended ACLs from file path
|
68
|
+
# @param path [String] path to file or directory
|
69
|
+
# @return [PosixACL] new acl of path
|
70
|
+
def self.clear_facl(path:)
|
71
|
+
path = Pathname.new path
|
72
|
+
raise InvalidPath, "invalid path: #{path}" unless path.exist?
|
73
|
+
_, err, s = Open3.capture3(SET_FACL_BIN, '-b', path.to_s)
|
74
|
+
raise BadExitCode, err unless s.success?
|
75
|
+
get_facl(path: path)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Generate an ACL by parsing a string along with options
|
79
|
+
# @param acl [#to_s] string describing acl
|
80
|
+
# @param kwargs [Hash] extra arguments defining acl
|
81
|
+
# @return [PosixACL] acl generated by string and options
|
82
|
+
def self.parse(acl, **kwargs)
|
83
|
+
entries = []
|
84
|
+
acl.to_s.strip.split(/\n|,/).grep(/^[^#]/).each do |entry|
|
85
|
+
entries << entry_class.parse(entry)
|
86
|
+
end
|
87
|
+
mask = entries.detect {|e| e.flag == :mask}
|
88
|
+
new(entries: entries - [mask], mask: mask, **kwargs)
|
89
|
+
end
|
90
|
+
|
91
|
+
# @param owner [#to_s] name of owner
|
92
|
+
# @param group [#to_s] name of group
|
93
|
+
# @param mask [PosixACL] mask permissions
|
94
|
+
# @see ACL#initialize
|
95
|
+
def initialize(owner:, group:, mask:, **kwargs)
|
96
|
+
super(kwargs.merge(default: false))
|
97
|
+
@owner = owner.to_s
|
98
|
+
@group = group.to_s
|
99
|
+
@mask = mask
|
100
|
+
end
|
101
|
+
|
102
|
+
# Check if queried principle has access to resource
|
103
|
+
# @param principle [User, Group] principle to check against
|
104
|
+
# @param permission [Symbol] permission to check against
|
105
|
+
# @return [Boolean] does principle have access?
|
106
|
+
def allow?(principle:, permission:)
|
107
|
+
# First check owner entry then check rest of user entries (order
|
108
|
+
# matters). If match, then this entry determines access.
|
109
|
+
entries.select(&:user_entry?).sort_by {|e| e.user_owner_entry? ? 0 : 1}.each do |entry|
|
110
|
+
return entry.has_permission?(permission: permission, mask: mask) if entry.match(principle: principle, owner: owner, group: group)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Then check groups (order independent). Entry only determines access
|
114
|
+
# if it contains requested permission.
|
115
|
+
groups = entries.select {|e| e.group_entry? && e.match(principle: principle, owner: owner, group: group)}.map do |entry|
|
116
|
+
entry.has_permission?(permission: permission, mask: mask)
|
117
|
+
end
|
118
|
+
|
119
|
+
unless groups.empty?
|
120
|
+
# Found matching groups so check if any give access
|
121
|
+
groups.any?
|
122
|
+
else
|
123
|
+
# Failed to find any matching groups so check "other" entry
|
124
|
+
entries.detect(&:other_entry?).has_permission?(permission: permission, mask: mask)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Convert object to string
|
129
|
+
# @return [String] the string describing the object
|
130
|
+
def to_s
|
131
|
+
(entries + [mask]).join(",")
|
132
|
+
end
|
133
|
+
|
134
|
+
# Convert object to hash
|
135
|
+
# @return [Hash] the hash describing this object
|
136
|
+
def to_h
|
137
|
+
super.merge owner: owner, group: group, mask: mask
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
# Use PosixEntry for entry objects
|
142
|
+
def self.entry_class
|
143
|
+
PosixEntry
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Object describing single Posix ACL entry
|
148
|
+
class PosixEntry < ACLEntry
|
149
|
+
# Valid flags for an ACL entry
|
150
|
+
VALID_FLAG = %i[ user group mask other ]
|
151
|
+
|
152
|
+
# Valid permissions for an ACL entry
|
153
|
+
VALID_PERMISSION = %i[ r w x ]
|
154
|
+
|
155
|
+
# Regular expression used when parsing ACL entry string
|
156
|
+
REGEX_PATTERN = %r[^(?<default>default:)?(?<flag>#{VALID_FLAG.join('|')}):(?<principle>\w*):(?<permissions>[#{VALID_PERMISSION.join}\-]{3})]
|
157
|
+
|
158
|
+
# Is this a default ACL entry
|
159
|
+
# @return [Boolean] whether default acl entry
|
160
|
+
attr_reader :default
|
161
|
+
|
162
|
+
# Flag set on ACL entry
|
163
|
+
# @return [Symbol] flag on acl entry
|
164
|
+
attr_reader :flag
|
165
|
+
|
166
|
+
# Permissions of ACL entry
|
167
|
+
# @return [Array<Symbol>] permissions of acl entry
|
168
|
+
attr_reader :permissions
|
169
|
+
|
170
|
+
# @param default [Boolean] whether default acl entry
|
171
|
+
# @param flag [#to_sym] flag for entry
|
172
|
+
# @param permissions [Array<#to_sym>] list of permissions for entry
|
173
|
+
# @see ACLEntry#initialize
|
174
|
+
def initialize(default: false, flag:, permissions:, **kwargs)
|
175
|
+
@default = default
|
176
|
+
@flag = flag.to_sym
|
177
|
+
@permissions = permissions.map(&:to_sym)
|
178
|
+
super(kwargs)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Do the requested args match this ACL entry?
|
182
|
+
# @param principle [User, Group, #to_s] requested principle
|
183
|
+
# @param owner [String] owner of corresponding ACL
|
184
|
+
# @param group [String] owning group of corresponding ACL
|
185
|
+
# @raise [ArgumentError] principle isn't {User} or {Group} object
|
186
|
+
# @return [Boolean] does this match this entry
|
187
|
+
def match(principle:, owner:, group:)
|
188
|
+
principle = User.new(principle) if (!principle.is_a?(User) && !principle.is_a?(Group))
|
189
|
+
return false if default_entry?
|
190
|
+
p = self.principle
|
191
|
+
p = owner if user_owner_entry?
|
192
|
+
p = group if group_owner_entry?
|
193
|
+
if (principle.is_a?(User) && group_entry?)
|
194
|
+
principle.groups.include?(p)
|
195
|
+
elsif (principle.is_a?(User) && user_entry?) || (principle.is_a?(Group) && group_entry?)
|
196
|
+
principle == p
|
197
|
+
elsif other_entry?
|
198
|
+
true
|
199
|
+
else
|
200
|
+
false
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Is this a default ACL entry
|
205
|
+
# @return [Boolean] is this a default entry
|
206
|
+
def default_entry?
|
207
|
+
default
|
208
|
+
end
|
209
|
+
|
210
|
+
# Is this a user-specific ACL entry
|
211
|
+
# @return [Boolean] is this a user entry
|
212
|
+
def user_entry?
|
213
|
+
!default_entry? && flag == :user
|
214
|
+
end
|
215
|
+
|
216
|
+
# Is this a group-specific ACL entry
|
217
|
+
# @return [Boolean] is this a group entry
|
218
|
+
def group_entry?
|
219
|
+
!default_entry? && flag == :group
|
220
|
+
end
|
221
|
+
|
222
|
+
# Is this an other-specific ACL entry
|
223
|
+
# @return [Boolean] is this an other entry
|
224
|
+
def other_entry?
|
225
|
+
!default_entry? && flag == :other
|
226
|
+
end
|
227
|
+
|
228
|
+
# Is this the owner ACL entry
|
229
|
+
# @return [Boolean] is this the owner entry
|
230
|
+
def user_owner_entry?
|
231
|
+
user_entry? && principle.empty?
|
232
|
+
end
|
233
|
+
|
234
|
+
# Is this the owning group ACL entry
|
235
|
+
# @return [Boolean] is this the owning group entry
|
236
|
+
def group_owner_entry?
|
237
|
+
group_entry? && principle.empty?
|
238
|
+
end
|
239
|
+
|
240
|
+
# Does this entry have the requested permission
|
241
|
+
# @param permission [#to_sym] the requested permission
|
242
|
+
# @param mask [PosixEntry] the permissions of the mask entry
|
243
|
+
# @return [Boolean] found this permission
|
244
|
+
def has_permission?(permission:, mask:)
|
245
|
+
if user_owner_entry? || other_entry?
|
246
|
+
permissions.include? permission.to_sym
|
247
|
+
else
|
248
|
+
(mask ? permissions & mask.permissions : permissions).include? permission.to_sym
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Convert object to string
|
253
|
+
# @param w_perms [Boolean] whether display permissions
|
254
|
+
# @return [String] the string describing this object
|
255
|
+
def to_s(w_perms: true)
|
256
|
+
%[#{"default:" if default_entry?}#{flag}:#{principle}#{":#{permissions.join}" if w_perms}]
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
# Parse an entry string into input parameters
|
261
|
+
def self.parse_entry(entry)
|
262
|
+
e = REGEX_PATTERN.match(entry.to_s.strip) do |m|
|
263
|
+
{
|
264
|
+
default: m[:default] ? true : false,
|
265
|
+
flag: m[:flag],
|
266
|
+
principle: m[:principle],
|
267
|
+
permissions: m[:permissions].chars
|
268
|
+
}
|
269
|
+
end
|
270
|
+
e ? e : raise(InvalidACLEntry, "invalid entry: #{entry}")
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module OodSupport
|
2
|
+
# The root exception class that all OodSupport-specific exceptions inherit
|
3
|
+
# from
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
# An exception raised when attempting to access a path that doesn't exist on
|
7
|
+
# local file system
|
8
|
+
class InvalidPath < Error; end
|
9
|
+
|
10
|
+
# An exception raised when attempting to run a command that exits with an
|
11
|
+
# exit code other than 0
|
12
|
+
class BadExitCode < Error; end
|
13
|
+
|
14
|
+
# An exception raised when attempting to parse an ACL entry from a string
|
15
|
+
class InvalidACLEntry < Error; end
|
16
|
+
end
|
data/lib/ood_support/group.rb
CHANGED
@@ -25,7 +25,7 @@ module OodSupport
|
|
25
25
|
end
|
26
26
|
|
27
27
|
# The comparison operator for sorting values
|
28
|
-
# @param other [
|
28
|
+
# @param other [#to_s] group to compare against
|
29
29
|
# @return [Fixnum] how groups compare
|
30
30
|
def <=>(other)
|
31
31
|
name <=> other
|
@@ -36,13 +36,13 @@ module OodSupport
|
|
36
36
|
# @param other [Group] group to compare against
|
37
37
|
# @return [Boolean] whether same objects
|
38
38
|
def eql?(other)
|
39
|
-
other.
|
39
|
+
self.class == other.class && self == other
|
40
40
|
end
|
41
41
|
|
42
42
|
# Generates a hash value for this object
|
43
43
|
# @return [Fixnum] hash value of object
|
44
44
|
def hash
|
45
|
-
|
45
|
+
[self.class, name].hash
|
46
46
|
end
|
47
47
|
|
48
48
|
# Convert object to string using group name as string value
|
data/lib/ood_support/user.rb
CHANGED
@@ -28,6 +28,8 @@ module OodSupport
|
|
28
28
|
|
29
29
|
alias_method :id, :uid
|
30
30
|
|
31
|
+
alias_method :home, :dir
|
32
|
+
|
31
33
|
# @param user [Fixnum, #to_s] user id or name
|
32
34
|
def initialize(user = Process.user)
|
33
35
|
@passwd = user.is_a?(Fixnum) ? Etc.getpwuid(user) : Etc.getpwnam(user.to_s)
|
@@ -40,6 +42,8 @@ module OodSupport
|
|
40
42
|
groups.include? Group.new(group)
|
41
43
|
end
|
42
44
|
|
45
|
+
alias_method :member_of_group?, :in_group?
|
46
|
+
|
43
47
|
# Provide primary group of user
|
44
48
|
# @return [Group] primary group of user
|
45
49
|
def group
|
@@ -53,7 +57,7 @@ module OodSupport
|
|
53
57
|
end
|
54
58
|
|
55
59
|
# The comparison operator for sorting values
|
56
|
-
# @param other [
|
60
|
+
# @param other [#to_s] user to compare against
|
57
61
|
# @return [Fixnum] how users compare
|
58
62
|
def <=>(other)
|
59
63
|
name <=> other
|
@@ -64,13 +68,13 @@ module OodSupport
|
|
64
68
|
# @param other [User] user to compare against
|
65
69
|
# @return [Boolean] whether same objects
|
66
70
|
def eql?(other)
|
67
|
-
other.
|
71
|
+
self.class == other.class && self == other
|
68
72
|
end
|
69
73
|
|
70
74
|
# Generates a hash value for this object
|
71
75
|
# @return [Fixnum] hash value of object
|
72
76
|
def hash
|
73
|
-
|
77
|
+
[self.class, name].hash
|
74
78
|
end
|
75
79
|
|
76
80
|
# Convert object to string using user name as string value
|
data/lib/ood_support/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ood_support
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Nicklas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-08-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -47,11 +47,17 @@ extensions: []
|
|
47
47
|
extra_rdoc_files: []
|
48
48
|
files:
|
49
49
|
- ".gitignore"
|
50
|
+
- CHANGELOG.md
|
50
51
|
- Gemfile
|
51
52
|
- LICENSE.txt
|
52
53
|
- README.md
|
53
54
|
- Rakefile
|
54
55
|
- lib/ood_support.rb
|
56
|
+
- lib/ood_support/acl.rb
|
57
|
+
- lib/ood_support/acl_entry.rb
|
58
|
+
- lib/ood_support/acls/nfs4.rb
|
59
|
+
- lib/ood_support/acls/posix.rb
|
60
|
+
- lib/ood_support/errors.rb
|
55
61
|
- lib/ood_support/group.rb
|
56
62
|
- lib/ood_support/process.rb
|
57
63
|
- lib/ood_support/user.rb
|