icfs 0.1.1 → 0.1.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.
data/lib/icfs/store.rb CHANGED
@@ -220,8 +220,7 @@ class Store
220
220
  # Path for case
221
221
  #
222
222
  def _case(cid, lnum)
223
- [
224
- @base,
223
+ @base + [
225
224
  cid,
226
225
  'c'.freeze,
227
226
  '%d.json'.freeze % lnum
@@ -233,8 +232,7 @@ class Store
233
232
  # Path for log
234
233
  #
235
234
  def _log(cid, lnum)
236
- [
237
- @base,
235
+ @base + [
238
236
  cid,
239
237
  'l'.freeze,
240
238
  '%d.json'.freeze % lnum
@@ -246,8 +244,7 @@ class Store
246
244
  # Path for entry
247
245
  #
248
246
  def _entry(cid, enum, lnum)
249
- [
250
- @base,
247
+ @base + [
251
248
  cid,
252
249
  'e'.freeze,
253
250
  enum.to_s,
@@ -260,8 +257,7 @@ class Store
260
257
  # Path for file
261
258
  #
262
259
  def _file(cid, enum, lnum, fnum)
263
- [
264
- @base,
260
+ @base + [
265
261
  cid,
266
262
  'e'.freeze,
267
263
  enum.to_s,
@@ -274,8 +270,7 @@ class Store
274
270
  # Filename for action
275
271
  #
276
272
  def _action(cid, anum, lnum)
277
- [
278
- @base,
273
+ @base + [
279
274
  cid,
280
275
  'a'.freeze,
281
276
  anum.to_s,
@@ -288,8 +283,7 @@ class Store
288
283
  # Filename for index
289
284
  #
290
285
  def _index(cid, xnum, lnum)
291
- [
292
- @base,
286
+ @base + [
293
287
  cid,
294
288
  'i'.freeze,
295
289
  xnum.to_s,
data/lib/icfs/store_fs.rb CHANGED
@@ -29,7 +29,11 @@ class StoreFs < Store
29
29
  # @param base [String] the base directory
30
30
  #
31
31
  def initialize(base)
32
- @base = base.freeze
32
+ if base[-1] == '/'.freeze
33
+ @base = base.freeze
34
+ else
35
+ @base = (base + '/').freeze
36
+ end
33
37
  end
34
38
 
35
39
 
data/lib/icfs/users.rb CHANGED
@@ -16,11 +16,8 @@ module ICFS
16
16
  ##########################################################################
17
17
  # User, Role, Group, and Global Perms
18
18
  #
19
- #
20
19
  # @abstract
21
20
  #
22
- # @todo Add cache flush method
23
- #
24
21
  class Users
25
22
 
26
23
  ###############################################
data/lib/icfs/users_fs.rb CHANGED
@@ -11,7 +11,9 @@
11
11
 
12
12
  require 'tempfile'
13
13
  require 'fileutils'
14
- require 'set'
14
+ require 'json'
15
+
16
+ require_relative 'users'
15
17
 
16
18
  module ICFS
17
19
 
@@ -20,22 +22,6 @@ module ICFS
20
22
  #
21
23
  class UsersFs < Users
22
24
 
23
- private
24
-
25
- ###############################################
26
- # read a raw file
27
- def _read(fn)
28
- json = File.read(File.join(@path, fn + '.json'.freeze))
29
- obj = Items.parse(json, 'User/Role/Group'.freeze, Users::ValUser)
30
- if obj['name'] != fn
31
- raise(Error::Values, 'UsersFs user %s name mismatch'.freeze % fn)
32
- end
33
- return obj
34
- end # _read
35
-
36
-
37
- public
38
-
39
25
 
40
26
  ###############################################
41
27
  # New instance
@@ -47,63 +33,26 @@ class UsersFs < Users
47
33
  end
48
34
 
49
35
 
36
+ ###############################################
37
+ # Path to store the file
38
+ #
39
+ def _path(urg)
40
+ File.join(@path, urg + '.json'.freeze)
41
+ end
42
+ private :_path
43
+
44
+
50
45
  ###############################################
51
46
  # (see Users#read)
52
47
  #
53
48
  def read(urg)
54
- Items.validate(urg, 'User/Role/Group'.freeze, Items::FieldUsergrp)
55
-
56
- # get the base user
57
- usr = _read(urg)
58
- return usr if usr['type'] != 'user'.freeze
59
-
60
- # assemble
61
- type = usr['type']
62
- done_s = Set.new.add(urg)
63
- ary = []
64
- roles_s = Set.new
65
- if usr['roles']
66
- ary.concat usr['roles']
67
- roles_s.merge usr['roles']
68
- end
69
- grps_s = Set.new
70
- if usr['groups']
71
- ary.concat usr['groups']
72
- grps_s.merge usr['groups']
73
- end
74
- perms_s = Set.new
75
- if usr['perms']
76
- perms_s.merge usr['perms']
77
- end
78
-
79
- # roles & groups
80
- while itm = ary.shift
81
- next if done_s.include?(itm)
82
- done_s.add(itm)
83
-
84
- usr = _read(itm)
85
- if usr['roles']
86
- ary.concat usr['roles']
87
- roles_s.merge usr['roles']
88
- end
89
- if usr['groups']
90
- ary.concat usr['groups']
91
- grps_s.merge usr['groups']
92
- end
93
- if usr['perms']
94
- perms_s.merge usr['perms']
95
- end
49
+ Items.validate(urg, 'User/Role/Group name'.freeze, Items::FieldUsergrp)
50
+ json = File.read(_path(urg))
51
+ obj = Items.parse(json, 'User/Role/Group'.freeze, Users::ValUser)
52
+ if obj['name'] != urg
53
+ raise(Error::Values, 'UsersFs user %s name mismatch'.freeze % fn)
96
54
  end
97
-
98
- # assemble final
99
- return {
100
- 'name' => urg.dup,
101
- 'type' => type,
102
- 'roles' => roles_s.to_a,
103
- 'groups' => grps_s.to_a,
104
- 'perms' => perms_s.to_a,
105
- }
106
-
55
+ return obj
107
56
  rescue Errno::ENOENT
108
57
  return nil
109
58
  end # def read()
@@ -122,7 +71,7 @@ class UsersFs < Users
122
71
  tmp.close
123
72
 
124
73
  # move
125
- FileUtils.mv(tmp.path, File.join(@path, obj['name'] + '.json'.freeze))
74
+ FileUtils.mv(tmp.path, _path(obj['name']))
126
75
  tmp.unlink
127
76
  end # def write()
128
77
 
@@ -0,0 +1,127 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License version 3.
8
+ #
9
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
10
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+
12
+ require 'redis'
13
+
14
+ module ICFS
15
+
16
+ ##########################################################################
17
+ # Implement Users with a Redis cache
18
+ #
19
+ class UsersRedis < Users
20
+
21
+ ###############################################
22
+ # New instance
23
+ #
24
+ # @param redis [Redis] The redis client
25
+ # @param base [Users] The base Users store
26
+ # @param opts [Hash] Options
27
+ # @option opts [String] :prefix Prefix for Redis key
28
+ # @option opts [Integer] :expires Expiration time in seconds
29
+ #
30
+ def initialize(redis, base, opts={})
31
+ @redis = redis
32
+ @base = base
33
+ @pre = opts[:prefix] || ''.freeze
34
+ @exp = opts[:expires] || 1*60*60 # 1 hour default
35
+ end
36
+
37
+
38
+ ###############################################
39
+ # Where to store in Redis
40
+ #
41
+ def _key(urg)
42
+ @pre + urg
43
+ end
44
+ private :_key
45
+
46
+
47
+ ###############################################
48
+ # (see Users#read)
49
+ #
50
+ def read(urg)
51
+ Validate.check(urg, Items::FieldUsergrp) # FIXME
52
+ key = _key(urg)
53
+
54
+ # try cache
55
+ json = @redis.get(key)
56
+ return JSON.parse(json) if json
57
+
58
+ # get base object from base store
59
+ bse = @base.read(urg)
60
+ return nil if !bse
61
+
62
+ # assemble
63
+ seen = Set.new.add(urg)
64
+ ary = []
65
+ roles = Set.new
66
+ grps = Set.new
67
+ perms = Set.new
68
+ if bse['roles']
69
+ ary.concat bse['roles']
70
+ roles.merge bse['roles']
71
+ end
72
+ if bse['groups']
73
+ ary.concat bse['groups']
74
+ grps.merge bse['groups']
75
+ end
76
+ if bse['perms']
77
+ perms.merge bse['perms']
78
+ end
79
+
80
+ # call ourself recursively for any un-expanded memberships
81
+ while itm = ary.shift
82
+ next if seen.include?(itm)
83
+ seen.add(itm)
84
+ ikey = _key(itm)
85
+
86
+ # all included u/r/g have been seen & expanded
87
+ mem = self.read(itm)
88
+ next if !mem
89
+ if mem['roles']
90
+ roles.merge mem['roles']
91
+ seen.merge mem['roles']
92
+ end
93
+ if mem['groups']
94
+ grps.merge mem['groups']
95
+ seen.merge mem['groups']
96
+ end
97
+ if mem['perms']
98
+ perms.merge mem['perms']
99
+ end
100
+ end
101
+
102
+ # final result
103
+ bse['roles'] = roles.to_a unless roles.empty?
104
+ bse['groups'] = grps.to_a unless grps.empty?
105
+ bse['perms'] = perms.to_a unless perms.empty?
106
+ json = JSON.pretty_generate(bse)
107
+
108
+ # save to cache
109
+ @redis.set(key, json)
110
+ @redis.expire(key, @exp)
111
+ return bse
112
+ end # def read()
113
+
114
+
115
+ ###############################################
116
+ # (see Users#write)
117
+ #
118
+ def write(obj)
119
+ json = Items.generate(obj, 'User/Role/Group'.freeze, Users::ValUser)
120
+ key = _key(obj['name'])
121
+ @redis.del(key)
122
+ @base.write(obj)
123
+ end # def write()
124
+
125
+ end # class ICFS::UsersRedis
126
+
127
+ end # module ICFS
@@ -0,0 +1,68 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License version 3.
8
+ #
9
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
10
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+
12
+ require 'aws-sdk-s3'
13
+
14
+ module ICFS
15
+
16
+ ##########################################################################
17
+ # Implements {ICFS::Users Users} from AWS S3
18
+ #
19
+ class UsersS3 < Users
20
+
21
+ ###############################################
22
+ # New instance
23
+ #
24
+ # @param s3 [Aws::S3::Client] the configured S3 client
25
+ # @param bucket [String] The bucket name
26
+ # @param prefix [String] Prefix to use for object keys
27
+ #
28
+ def initialize(s3, bucket, prefix=nil)
29
+ @s3 = s3
30
+ @bck = bucket
31
+ @pre = prefix || ''.freeze
32
+ end
33
+
34
+
35
+ ###############################################
36
+ # Where to store onfig
37
+ #
38
+ def _path(user)
39
+ @pre + user
40
+ end # def _path()
41
+ private :_path
42
+
43
+
44
+ ###############################################
45
+ # (see Users#read)
46
+ #
47
+ def read(urg)
48
+ Items.validate(urg, 'User/Role/Group name'.freeze, Items::FieldUsergrp)
49
+ json = @s3.get_object( bucket: @bck, key: _path(urg) ).body.read
50
+ return JSON.parse(json)
51
+ rescue
52
+ return nil
53
+ end # def read()
54
+
55
+
56
+ ###############################################
57
+ # (see Users#write)
58
+ #
59
+ def write(obj)
60
+ Items.validate(obj, 'User/Role/Group'.freeze, Users::ValUser)
61
+ json = JSON.pretty_generate(obj)
62
+ @s3.put_object( bucket: @bck, key: _path(obj['name']), body: json )
63
+ end # def write()
64
+
65
+
66
+ end # class ICFS::UsersS3
67
+
68
+ end # module ICFS
@@ -10,6 +10,7 @@
10
10
  # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
11
 
12
12
  require_relative '../store_fs'
13
+
13
14
  #
14
15
  module ICFS
15
16
 
@@ -20,6 +21,7 @@ module Utils
20
21
  #
21
22
  class Backup
22
23
 
24
+
23
25
  ###############################################
24
26
  # Instance
25
27
  #
@@ -37,7 +39,7 @@ class Backup
37
39
  ###############################################
38
40
  # Copy an item
39
41
  #
40
- def _item(dest, title, read, write, args, val=nil)
42
+ def _copy_item(dest, title, read, write, args, val=nil)
41
43
  @log.debug('ICFS copy: %s'.freeze % title)
42
44
 
43
45
  # read the item
@@ -65,7 +67,7 @@ class Backup
65
67
  @log.error('ICFS copy: %s bad JSON'.freeze % title)
66
68
  return nil
67
69
  end # def _item
68
- private :_item
70
+ private :_copy_item
69
71
 
70
72
 
71
73
  ###############################################
@@ -76,8 +78,16 @@ class Backup
76
78
  # @param lnum_max [Integer] The highest log
77
79
  # @param lnum_min [Integer] The lowest log
78
80
  #
79
- def copy(cid, dest, lnum_max, lnum_min=1)
81
+ def copy(cid, dest, lnum_min=1, lnum_max=0)
80
82
  @log.info('ICFS copy: %s %d-%d'.freeze % [cid, lnum_min, lnum_max])
83
+
84
+ # if no max specified, pull from current
85
+ if lnum_max == 0
86
+ json = @cache.current_read(cid)
87
+ cur = Items.parse(json, 'current'.freeze, Items::ItemCurrent)
88
+ lnum_max = cur['log']
89
+ end
90
+
81
91
  if lnum_min > lnum_max
82
92
  raise ArgumentError, 'ICFS copy, log num min is larger than max'.freeze
83
93
  end
@@ -105,7 +115,7 @@ class Backup
105
115
  'entry %d-%d'.freeze % [enum, lnum],
106
116
  :entry_read,
107
117
  :entry_write,
108
- [cid, lnum]
118
+ [cid, enum, lnum]
109
119
  )
110
120
 
111
121
  # index
@@ -121,7 +131,7 @@ class Backup
121
131
 
122
132
  # action
123
133
  if log['action']
124
- anum = log['action']['lnum']
134
+ anum = log['action']['num']
125
135
  _copy_item(dest,
126
136
  'action %d-%d'.freeze % [anum, lnum],
127
137
  :action_read,
@@ -133,7 +143,7 @@ class Backup
133
143
  # case
134
144
  if log['case_hash']
135
145
  _copy_item(dest,
136
- 'case %d'.freeze, % lnum,
146
+ 'case %d'.freeze % lnum,
137
147
  :case_read,
138
148
  :case_write,
139
149
  [cid, lnum]
@@ -142,7 +152,8 @@ class Backup
142
152
 
143
153
  # files
144
154
  if log['files_hash']
145
- log['files_hash'].each_index do |fnum|
155
+ log['files_hash'].each_index do |fraw|
156
+ fnum = fraw + 1
146
157
 
147
158
  @log.debug('ICFS copy: file %d-%d-%d'.freeze % [enum, lnum, fnum])
148
159
 
@@ -170,6 +181,200 @@ class Backup
170
181
  end # def copy()
171
182
 
172
183
 
184
+ ###############################################
185
+ # Restore an item
186
+ #
187
+ def _restore_item(src, title, read, write, args_st, args_ca, val=nil)
188
+ @log.debug('ICFS restore: %s'.freeze % title)
189
+
190
+ # read the item
191
+ json = src.send(read, *args_st)
192
+ if !json
193
+ @log.warn('ICFS restore: %s is missing'.freeze % title)
194
+ return nil
195
+ end
196
+
197
+ # parse item if requested
198
+ if val
199
+ obj = JSON.parse(json)
200
+ err = Validate.check(obj, val)
201
+ if err
202
+ @log.error('ICFS restore: %s bad format'.freeze % title)
203
+ return nil
204
+ end
205
+ end
206
+
207
+ # write the item to store & cache
208
+ @store.send(write, *args_st, json)
209
+ @cache.send(write, *args_ca, json)
210
+ return [obj, json]
211
+
212
+ end # def _restore_item()
213
+ private :_restore_item
214
+
215
+
216
+ ###############################################
217
+ # Restore a backup into a case
218
+ #
219
+ # @param cid [String] The case ID
220
+ # @param src [Store] Source store
221
+ # @param lnum_max [Integer] The highest log
222
+ # @param lnum_min [Integer] The lowest log
223
+ #
224
+ def restore(cid, src, lnum_min=0, lnum_max=0)
225
+ @log.info('ICFS restore: %s %d-%d'.freeze % [cid, lnum_min, lnum_max])
226
+
227
+ # take lock
228
+ @cache.lock_take(cid)
229
+ begin
230
+
231
+ # read current
232
+ json = @cache.current_read(cid)
233
+ if json
234
+ cur = Items.parse(json, 'current'.freeze, Items::ItemCurrent)
235
+ else
236
+ cur = {
237
+ 'icfs' => 1,
238
+ 'caseid' => cid,
239
+ 'log' => 1,
240
+ 'entry' => 1,
241
+ 'action' => 0,
242
+ 'index' => 0
243
+ }
244
+ end
245
+
246
+ # if no min specified, pull from current or default to 1
247
+ lnum_min = cur['log'] if lnum_min == 0
248
+
249
+ # sanity check min & max
250
+ if (lnum_min > lnum_max) && (lnum_max != 0)
251
+ raise ArgumentError: 'ICFS restore, log min is larger than max'.freeze
252
+ end
253
+
254
+ # max entry, action, index
255
+ emax = cur['entry']
256
+ amax = cur['action']
257
+ imax = cur['index']
258
+
259
+ # each log
260
+ lnum = lnum_min
261
+ llast = nil
262
+ while lnum != lnum_max
263
+
264
+ # copy the log
265
+ log, litem = _restore_item(src,
266
+ 'log %d'.freeze % lnum,
267
+ :log_read,
268
+ :log_write,
269
+ [cid, lnum],
270
+ [cid, lnum],
271
+ Items::ItemLog
272
+ )
273
+
274
+ # no log - all done
275
+ if !log
276
+ break
277
+ else
278
+ llast = litem
279
+ end
280
+
281
+ # entry
282
+ enum = log['entry']['num']
283
+ _restore_item(src,
284
+ 'entry %d-%d'.freeze % [enum, lnum],
285
+ :entry_read,
286
+ :entry_write,
287
+ [cid, enum, lnum],
288
+ [cid, enum]
289
+ )
290
+ emax = enum if enum > emax
291
+
292
+ # index
293
+ if log['index']
294
+ xnum = log['index']['num']
295
+ _restore_item(src,
296
+ 'index %d-%d'.freeze % [xnum, lnum],
297
+ :index_read,
298
+ :index_write,
299
+ [cid, xnum, lnum],
300
+ [cid, xnum]
301
+ )
302
+ imax = xnum if xnum > imax
303
+ end
304
+
305
+ # action
306
+ if log['action']
307
+ anum = log['action']['num']
308
+ _restore_item(src,
309
+ 'action %d-%d'.freeze % [anum, lnum],
310
+ :action_read,
311
+ :action_write,
312
+ [cid, anum, lnum],
313
+ [cid, anum]
314
+ )
315
+ amax = anum if anum > amax
316
+ end
317
+
318
+ # case
319
+ if log['case_hash']
320
+ _restore_item(src,
321
+ 'case %d'.freeze % lnum,
322
+ :case_read,
323
+ :case_write,
324
+ [cid, lnum],
325
+ [cid]
326
+ )
327
+ end
328
+
329
+ # files
330
+ if log['files_hash']
331
+ log['files_hash'].each_index do |fraw|
332
+ fnum = fraw + 1
333
+
334
+ @log.debug('ICFS restore: file %d-%d-%d'.freeze % [enum, lnum, fnum])
335
+
336
+ # read
337
+ fi = src.file_read(cid, enum, lnum, fnum)
338
+ if !fi
339
+ @log.warn('ICFS restore: file %d-%d-%d missing'.freeze %
340
+ [enum, lnum, fnum])
341
+ next
342
+ end
343
+
344
+ # copy
345
+ tmp = @store.tempfile
346
+ IO.copy_stream(fi, tmp)
347
+ src.close(fi)
348
+
349
+ # write
350
+ @store.file_write(cid, enum, lnum, fnum, tmp)
351
+ end
352
+ end
353
+
354
+ lnum += 1
355
+ end
356
+
357
+ # write current
358
+ cur = {
359
+ 'icfs' => 1,
360
+ 'caseid' => cid,
361
+ 'log' => lnum-1,
362
+ 'entry' => emax,
363
+ 'action' => amax,
364
+ 'index' => imax,
365
+ 'hash' => ICFS.hash(llast)
366
+ }
367
+ nitem = Items.generate(cur, 'current'.freeze, Items::ItemCurrent)
368
+ @cache.current_write(cid, nitem)
369
+
370
+ ensure
371
+ # release lock
372
+ @cache.lock_release(cid)
373
+ end
374
+
375
+ end # def restore()
376
+
377
+
173
378
  end # class ICFS::Utils::Backup
174
379
 
175
380
  end # module ICFS::Utils
@@ -16,16 +16,12 @@ module ICFS
16
16
  ##########################################################################
17
17
  # Low level utilities for working directly with ICFS systems.
18
18
  #
19
- # @todo Archive/move case tool written
20
- #
21
19
  module Utils
22
20
 
23
21
 
24
22
  ##########################################################################
25
23
  # Check a case for errors
26
24
  #
27
- # @todo Make a function to auto find the last log
28
- #
29
25
  class Check
30
26
 
31
27
 
@@ -180,7 +176,7 @@ class Check
180
176
  ['index'.freeze, 1].freeze,
181
177
  ['log'.freeze, 2].freeze
182
178
  ]
183
- )
179
+ )
184
180
  idx_cur.add(xnum)
185
181
  end
186
182