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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3ab9a0231c41e21de5992f0ae3bcf0be3ed19638
4
- data.tar.gz: fce3ab5ef3dc87c42cb8a8af09136aa80705ed7e
3
+ metadata.gz: 74117abef87ed8fce6977b36e54d99795db90ba2
4
+ data.tar.gz: f82d7ee819ed4bacece8ea084a3bb472e47105e9
5
5
  SHA512:
6
- metadata.gz: 0611200202ea6a276b89927499a45c130d54a06d5a6eb865bcf2c413ddefa85458ea3cb887f639e3dea7d8b1a8616d5cdf27dec54b05f8a3a834cf8a86f51878
7
- data.tar.gz: 98f312e86afbe33ea1b6d1b5d1e9830feb10a28e149d3cfdbe7692744a7280397b2d2911f55b946daea9cc17a6ebc5ba25cf0e27bf8c0f5843261cdb9d8ef38e
6
+ metadata.gz: 61abe8f3a20b7d6a9b6c3094d7b3490459dbf6629efd2559c420981ebb7257f7d617399177319fa84801ec48cd13dc44bb336ea1e50e5a35feeac42b43c13cee
7
+ data.tar.gz: 173d29d517fc5f9da03cefe598e32a195301eb3752d7f982edf74997f0e2cfbc1d0591f4907a0fe7f091bd2e79aadb597f6dbec20ba7ca2eae2c5f1696958644
@@ -0,0 +1,12 @@
1
+ ## Unreleased
2
+
3
+ ## 0.0.2 (2016-08-26)
4
+
5
+ Bugfixes:
6
+
7
+ - added aliases `User#home` and `User#member_of_group?` for backwards
8
+ compatibility
9
+
10
+ ## 0.0.1 (2016-07-01)
11
+
12
+ Initial release!
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 )
@@ -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
- # Your code goes here...
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
@@ -25,7 +25,7 @@ module OodSupport
25
25
  end
26
26
 
27
27
  # The comparison operator for sorting values
28
- # @param other [Group] group to compare against
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.is_a?(Group) && id == other.id
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
- id.hash
45
+ [self.class, name].hash
46
46
  end
47
47
 
48
48
  # Convert object to string using group name as string value
@@ -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 [User] user to compare against
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.is_a?(User) && id == other.id
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
- id.hash
77
+ [self.class, name].hash
74
78
  end
75
79
 
76
80
  # Convert object to string using user name as string value
@@ -1,4 +1,4 @@
1
1
  module OodSupport
2
2
  # The current version of {OodSupport}
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
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.1
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-07-01 00:00:00.000000000 Z
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