icfs 0.1.0
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 +7 -0
- data/LICENSE.txt +674 -0
- data/bin/icfs_demo_create.rb +89 -0
- data/bin/icfs_demo_fcgi.rb +51 -0
- data/bin/icfs_demo_ssl_gen.rb +84 -0
- data/bin/icfs_demo_web.rb +50 -0
- data/bin/icfs_dev_todo.rb +20 -0
- data/data/demo_config.yml +94 -0
- data/data/icfs.css +475 -0
- data/data/icfs.js +458 -0
- data/lib/icfs.rb +109 -0
- data/lib/icfs/api.rb +1436 -0
- data/lib/icfs/cache.rb +254 -0
- data/lib/icfs/cache_elastic.rb +1154 -0
- data/lib/icfs/demo/auth.rb +74 -0
- data/lib/icfs/demo/static.rb +59 -0
- data/lib/icfs/demo/timezone.rb +38 -0
- data/lib/icfs/elastic.rb +83 -0
- data/lib/icfs/items.rb +653 -0
- data/lib/icfs/store.rb +278 -0
- data/lib/icfs/store_fs.rb +98 -0
- data/lib/icfs/store_s3.rb +97 -0
- data/lib/icfs/users.rb +80 -0
- data/lib/icfs/users_elastic.rb +166 -0
- data/lib/icfs/users_fs.rb +132 -0
- data/lib/icfs/validate.rb +479 -0
- data/lib/icfs/web/auth_ssl.rb +73 -0
- data/lib/icfs/web/client.rb +4498 -0
- metadata +77 -0
data/lib/icfs.rb
ADDED
@@ -0,0 +1,109 @@
|
|
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 'digest/sha2'
|
13
|
+
|
14
|
+
##########################################################################
|
15
|
+
# Investigative Case File System
|
16
|
+
#
|
17
|
+
# @todo Delete Items and move into this module
|
18
|
+
# @todo Verification tool written
|
19
|
+
# @todo Archive/move case tool written
|
20
|
+
#
|
21
|
+
module ICFS
|
22
|
+
|
23
|
+
|
24
|
+
# no tags
|
25
|
+
TagNone = '[none]'.freeze
|
26
|
+
|
27
|
+
# edits an action
|
28
|
+
TagAction = '[action]'.freeze
|
29
|
+
|
30
|
+
# edits an index
|
31
|
+
TagIndex = '[index]'.freeze
|
32
|
+
|
33
|
+
# edits the case
|
34
|
+
TagCase = '[case]'.freeze
|
35
|
+
|
36
|
+
|
37
|
+
# permission to read case
|
38
|
+
PermRead = '[read]'.freeze
|
39
|
+
|
40
|
+
# permission to write case
|
41
|
+
PermWrite = '[write]'.freeze
|
42
|
+
|
43
|
+
# permission to manage case
|
44
|
+
PermManage = '[manage]'.freeze
|
45
|
+
|
46
|
+
# permission to manage actions
|
47
|
+
PermAction = '[action]'.freeze
|
48
|
+
|
49
|
+
# global permission to search
|
50
|
+
PermSearch = '{[search]}'.freeze
|
51
|
+
|
52
|
+
|
53
|
+
# user group
|
54
|
+
UserCase = '[case]'.freeze
|
55
|
+
|
56
|
+
|
57
|
+
###############################################
|
58
|
+
# Hash a string
|
59
|
+
def self.hash(str)
|
60
|
+
Digest::SHA256.hexdigest(str)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
###############################################
|
65
|
+
# Hash a tempfile
|
66
|
+
#
|
67
|
+
def self.hash_temp(tf)
|
68
|
+
Digest::SHA256.file(tf.path).hexdigest
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
##########################################################################
|
75
|
+
# Error
|
76
|
+
#
|
77
|
+
module Error
|
78
|
+
|
79
|
+
|
80
|
+
##########################################################################
|
81
|
+
# Invalid values
|
82
|
+
class Value < ArgumentError; end
|
83
|
+
|
84
|
+
##########################################################################
|
85
|
+
# Item not found
|
86
|
+
class NotFound < RuntimeError; end
|
87
|
+
|
88
|
+
##########################################################################
|
89
|
+
# Do not have required permissions
|
90
|
+
class Perms < RuntimeError; end
|
91
|
+
|
92
|
+
##########################################################################
|
93
|
+
# Conflict with pre-existing values
|
94
|
+
class Conflict < RuntimeError; end
|
95
|
+
|
96
|
+
##########################################################################
|
97
|
+
# Interface errors
|
98
|
+
class Interface < RuntimeError; end
|
99
|
+
|
100
|
+
end # module ICFS::Error
|
101
|
+
|
102
|
+
end # module ICFS
|
103
|
+
|
104
|
+
require_relative 'icfs/validate'
|
105
|
+
require_relative 'icfs/cache'
|
106
|
+
require_relative 'icfs/store'
|
107
|
+
require_relative 'icfs/items'
|
108
|
+
require_relative 'icfs/api'
|
109
|
+
require_relative 'icfs/users'
|
data/lib/icfs/api.rb
ADDED
@@ -0,0 +1,1436 @@
|
|
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
|
+
#
|
13
|
+
module ICFS
|
14
|
+
|
15
|
+
##########################################################################
|
16
|
+
# Api
|
17
|
+
#
|
18
|
+
# @todo Add event logging
|
19
|
+
#
|
20
|
+
class Api
|
21
|
+
|
22
|
+
# Validate a size
|
23
|
+
ValSize = {
|
24
|
+
method: :integer,
|
25
|
+
min: 2,
|
26
|
+
max: 100
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
|
30
|
+
# Validate a page
|
31
|
+
ValPage = {
|
32
|
+
method: :integer,
|
33
|
+
min: 1,
|
34
|
+
max: 10
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
|
38
|
+
# Validate a purpose
|
39
|
+
ValPurpose = {
|
40
|
+
method: :string,
|
41
|
+
min: 1,
|
42
|
+
max: 32,
|
43
|
+
invalid: /[[:cntrl:]]/.freeze
|
44
|
+
}.freeze
|
45
|
+
|
46
|
+
|
47
|
+
###############################################
|
48
|
+
# New API
|
49
|
+
#
|
50
|
+
# @param stats [Array<String>] Global stats
|
51
|
+
# @param users [Users] the User/role/group interface
|
52
|
+
# @param cache [Cache] the cache
|
53
|
+
# @param store [Store] the store
|
54
|
+
#
|
55
|
+
def initialize(stats, users, cache, store)
|
56
|
+
@users = users
|
57
|
+
@cache = cache
|
58
|
+
@store = store
|
59
|
+
@gstats = stats.map{|st| st.dup.freeze }.freeze
|
60
|
+
reset
|
61
|
+
end # def initialize
|
62
|
+
|
63
|
+
|
64
|
+
###############################################
|
65
|
+
# Set the user
|
66
|
+
#
|
67
|
+
# @param uname [String] the user name
|
68
|
+
#
|
69
|
+
def user=(uname)
|
70
|
+
@user = uname.dup.freeze
|
71
|
+
urgp = @users.read(uname)
|
72
|
+
raise(Error::NotFound, 'User name not found'.freeze) if !urgp
|
73
|
+
raise(Error::Value, 'Not a user'.freeze) if urgp['type'] != 'user'
|
74
|
+
@roles = urgp['roles'].each{|rn| rn.freeze }
|
75
|
+
@groups = urgp['groups'].each{ |gn| gn.freeze }
|
76
|
+
@perms = urgp['perms'].each{|pn| pn.freeze }
|
77
|
+
|
78
|
+
@urg = Set.new
|
79
|
+
@urg.add user
|
80
|
+
@urg.merge roles
|
81
|
+
@urg.merge groups
|
82
|
+
@urg.freeze
|
83
|
+
|
84
|
+
@ur = Set.new
|
85
|
+
@ur.add user
|
86
|
+
@ur.merge roles
|
87
|
+
@ur.freeze
|
88
|
+
|
89
|
+
reset
|
90
|
+
end # def user=()
|
91
|
+
|
92
|
+
|
93
|
+
###############################################
|
94
|
+
# User
|
95
|
+
#
|
96
|
+
attr_reader :user
|
97
|
+
|
98
|
+
|
99
|
+
###############################################
|
100
|
+
# Roles
|
101
|
+
#
|
102
|
+
attr_reader :roles
|
103
|
+
|
104
|
+
|
105
|
+
###############################################
|
106
|
+
# Groups
|
107
|
+
#
|
108
|
+
attr_reader :groups
|
109
|
+
|
110
|
+
|
111
|
+
###############################################
|
112
|
+
# Global perms
|
113
|
+
#
|
114
|
+
attr_reader :perms
|
115
|
+
|
116
|
+
|
117
|
+
###############################################
|
118
|
+
# Globals stats
|
119
|
+
attr_reader :gstats
|
120
|
+
|
121
|
+
|
122
|
+
###############################################
|
123
|
+
# User, Roles, Groups set
|
124
|
+
#
|
125
|
+
attr_reader :urg
|
126
|
+
|
127
|
+
|
128
|
+
###############################################
|
129
|
+
# Reset the cached cases and access
|
130
|
+
#
|
131
|
+
def reset
|
132
|
+
@cases = {}
|
133
|
+
@access = {}
|
134
|
+
@actions = {}
|
135
|
+
@tasked = {}
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
###############################################
|
140
|
+
# Get a tempfile
|
141
|
+
#
|
142
|
+
def tempfile
|
143
|
+
@store.tempfile
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
###############################################
|
148
|
+
# Get a stats list
|
149
|
+
#
|
150
|
+
# @param cid [String] caseid
|
151
|
+
# @return [Set<String>] the stats, global and case
|
152
|
+
# @raise [Error::NotFound] if case not found
|
153
|
+
#
|
154
|
+
def stats_list(cid)
|
155
|
+
cse = case_read(cid)
|
156
|
+
stats = Set.new
|
157
|
+
stats.merge( cse['stats'] ) if cse['stats']
|
158
|
+
stats.merge( @gstats )
|
159
|
+
return stats
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
###############################################
|
164
|
+
# Get an access list
|
165
|
+
#
|
166
|
+
# @param cid [String] caseid
|
167
|
+
# @return [Set<String>] the perms granted the user for this case
|
168
|
+
# @raise [Error::NotFound] if case not found
|
169
|
+
#
|
170
|
+
def access_list(cid)
|
171
|
+
if !@access.key?(cid)
|
172
|
+
|
173
|
+
# get grants for the case
|
174
|
+
cse = case_read(cid)
|
175
|
+
al = Set.new
|
176
|
+
cse['access'].each do |ac|
|
177
|
+
gs = Set.new(ac['grant'])
|
178
|
+
al.add(ac['perm']) if @urg.intersect?(gs)
|
179
|
+
end
|
180
|
+
|
181
|
+
# higher perms imply lower ones
|
182
|
+
al.add(ICFS::PermRead) if al.include?(ICFS::PermManage)
|
183
|
+
al.add(ICFS::PermWrite) if al.include?(ICFS::PermAction)
|
184
|
+
al.add(ICFS::PermRead) if al.include?(ICFS::PermWrite)
|
185
|
+
|
186
|
+
# merge in global perms
|
187
|
+
al.merge @perms
|
188
|
+
|
189
|
+
@access[cid] = al
|
190
|
+
end
|
191
|
+
return @access[cid]
|
192
|
+
end # def access_list()
|
193
|
+
|
194
|
+
|
195
|
+
###############################################
|
196
|
+
# See if we are tasked
|
197
|
+
def tasked?(cid, anum)
|
198
|
+
id = '%s.%d'.freeze % [cid, anum]
|
199
|
+
unless @tasked.key?(id)
|
200
|
+
act = _action_read(cid, anum)
|
201
|
+
|
202
|
+
tasked = false
|
203
|
+
act['tasks'].each do |tk|
|
204
|
+
if @ur.include?(tk['assigned'])
|
205
|
+
tasked = true
|
206
|
+
break
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
@tasked[id] = tasked
|
211
|
+
end
|
212
|
+
|
213
|
+
return @tasked[id]
|
214
|
+
end # def tasked?()
|
215
|
+
|
216
|
+
|
217
|
+
###############################################
|
218
|
+
# Check if we can read an entry or action
|
219
|
+
def _can_read?(cid, anum)
|
220
|
+
|
221
|
+
# have read permission on the case or
|
222
|
+
# are assigned to the action
|
223
|
+
if access_list(cid).include?( ICFS::PermRead )
|
224
|
+
return true
|
225
|
+
elsif anum && tasked?(cid, anum)
|
226
|
+
return true
|
227
|
+
else
|
228
|
+
return false
|
229
|
+
end
|
230
|
+
|
231
|
+
# handle an action that isn't found
|
232
|
+
rescue Error::NotFound
|
233
|
+
return false
|
234
|
+
end # def _can_read?()
|
235
|
+
private :_can_read?
|
236
|
+
|
237
|
+
|
238
|
+
###############################################
|
239
|
+
# Check if we have search permissions for a case
|
240
|
+
#
|
241
|
+
def _search?(query)
|
242
|
+
if( @perms.include?(ICFS::PermSearch) || ( query[:caseid] &&
|
243
|
+
access_list(query[:caseid]).include?(ICFS::PermRead) ) )
|
244
|
+
return true
|
245
|
+
else
|
246
|
+
return false
|
247
|
+
end
|
248
|
+
end # def _search?()
|
249
|
+
private :_search?
|
250
|
+
|
251
|
+
|
252
|
+
##############################################################
|
253
|
+
# Read Items
|
254
|
+
##############################################################
|
255
|
+
|
256
|
+
|
257
|
+
###############################################
|
258
|
+
# Read a case
|
259
|
+
#
|
260
|
+
# @param cid [String] caseid
|
261
|
+
# @param lnum [Integer] log it was recorded
|
262
|
+
# @return [Case] the case
|
263
|
+
# @raise [Error::NotFound] if not case not found
|
264
|
+
#
|
265
|
+
def case_read(cid, lnum=0)
|
266
|
+
if lnum != 0
|
267
|
+
json = @store.case_read(cid, lnum)
|
268
|
+
return Validate.parse(json, 'case'.freeze, Items::ItemCase)
|
269
|
+
end
|
270
|
+
|
271
|
+
if !@cases.key?(cid)
|
272
|
+
json = @cache.case_read(cid)
|
273
|
+
cur = Validate.parse(json, 'case'.freeze, Items::ItemCase)
|
274
|
+
@cases[cid] = cur
|
275
|
+
end
|
276
|
+
return @cases[cid]
|
277
|
+
end # end case_read()
|
278
|
+
|
279
|
+
|
280
|
+
###############################################
|
281
|
+
# Read a log
|
282
|
+
#
|
283
|
+
# @param cid [String] caseid
|
284
|
+
# @param lnum [Integer] log number
|
285
|
+
# @raise [Error::NotFound] if log is not found
|
286
|
+
# @raise [Error::Perms] if user does not have permissions
|
287
|
+
#
|
288
|
+
def log_read(cid, lnum)
|
289
|
+
|
290
|
+
# get access list
|
291
|
+
al = access_list(cid)
|
292
|
+
if !al.include?(ICFS::PermRead)
|
293
|
+
raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead)
|
294
|
+
end
|
295
|
+
|
296
|
+
# read
|
297
|
+
json = @cache.log_read(cid, lnum)
|
298
|
+
return Validate.parse(json, 'log'.freeze, Items::ItemLog)
|
299
|
+
end # def log_read()
|
300
|
+
|
301
|
+
|
302
|
+
###############################################
|
303
|
+
# Read an entry
|
304
|
+
#
|
305
|
+
# @param cid [String] caseid
|
306
|
+
# @param enum [Integer] the entry number
|
307
|
+
# @param lnum [Integer] the log number or 0 for current
|
308
|
+
# @raise [Error::NotFound] if it does not exist
|
309
|
+
# @raise [Error::Perms] if user does not have permissions
|
310
|
+
#
|
311
|
+
def entry_read(cid, enum, lnum=0)
|
312
|
+
|
313
|
+
# get access list and current entry
|
314
|
+
al = access_list(cid)
|
315
|
+
json = @cache.entry_read(cid, enum)
|
316
|
+
ec = Validate.parse(json, 'entry'.freeze, Items::ItemEntry)
|
317
|
+
|
318
|
+
# see if we can read the entry
|
319
|
+
need = Set.new
|
320
|
+
need.add( ICFS::PermRead ) unless _can_read?(cid, ec['action'] )
|
321
|
+
need.merge(ec['perms']) if ec['perms']
|
322
|
+
need.subtract(al)
|
323
|
+
unless need.empty?
|
324
|
+
raise(Error::Perms, 'missing perms: %s'.freeze %
|
325
|
+
need.to_a.sort.join(', ') )
|
326
|
+
end
|
327
|
+
|
328
|
+
# return requested version
|
329
|
+
if( lnum == 0 || ec['log'] == lnum )
|
330
|
+
return ec
|
331
|
+
else
|
332
|
+
json = @store.entry_read(cid, enum, lnum)
|
333
|
+
return Validate.parse(json, 'entry'.freeze, Items::ItemEntry)
|
334
|
+
end
|
335
|
+
end # def entry_read()
|
336
|
+
|
337
|
+
|
338
|
+
###############################################
|
339
|
+
# Read a file
|
340
|
+
#
|
341
|
+
# @param cid [String] caseid
|
342
|
+
# @param enum [Integer] the entry number
|
343
|
+
# @param lnum [Integer] the log number
|
344
|
+
# @param fnum [Integer] the file number
|
345
|
+
# @raise [Error::NotFound] if it does not exist
|
346
|
+
# @raise [Error::Perms] if user does not have permissions
|
347
|
+
#
|
348
|
+
def file_read(cid, enum, lnum, fnum)
|
349
|
+
entry_read(cid, enum)
|
350
|
+
fi = @store.file_read(cid, enum, lnum, fnum)
|
351
|
+
raise(Error::NotFound, 'file not found'.freeze) if !fi
|
352
|
+
return fi
|
353
|
+
end # def file_read()
|
354
|
+
|
355
|
+
|
356
|
+
###############################################
|
357
|
+
# Read an action
|
358
|
+
#
|
359
|
+
# Internal version.
|
360
|
+
#
|
361
|
+
def _action_read(cid, anum)
|
362
|
+
id = '%s.%d'.freeze % [cid, anum]
|
363
|
+
unless @actions.key?(id)
|
364
|
+
json = @cache.action_read(cid, anum)
|
365
|
+
act = Validate.parse(json, 'action'.freeze, Items::ItemAction)
|
366
|
+
@actions[id] = act
|
367
|
+
end
|
368
|
+
return @actions[id]
|
369
|
+
end # _action_read()
|
370
|
+
private :_action_read
|
371
|
+
|
372
|
+
|
373
|
+
###############################################
|
374
|
+
# Read an action
|
375
|
+
#
|
376
|
+
# @param cid [String] caseid
|
377
|
+
# @param anum [Integer] the action number
|
378
|
+
# @param lnum [Integer] the log number or 0 for current
|
379
|
+
# @return [Action] requested action
|
380
|
+
# @raise [Error::NotFound] if action is not found
|
381
|
+
# @raise [Error::Perms] if user does not have permissions
|
382
|
+
#
|
383
|
+
def action_read(cid, anum, lnum=0)
|
384
|
+
|
385
|
+
# get current action
|
386
|
+
ac = _action_read(cid, anum)
|
387
|
+
|
388
|
+
# see if we can read the action
|
389
|
+
unless _can_read?(cid, anum)
|
390
|
+
raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead )
|
391
|
+
end
|
392
|
+
|
393
|
+
# return the requested version
|
394
|
+
if( lnum == 0 || ac['log'] == lnum )
|
395
|
+
return ac
|
396
|
+
else
|
397
|
+
json = @store.action_read( cid, anum, lnum)
|
398
|
+
return Validate.parse(json, 'action'.freeze, Items::ItemAction)
|
399
|
+
end
|
400
|
+
end # def action_read()
|
401
|
+
|
402
|
+
|
403
|
+
###############################################
|
404
|
+
# Read an index
|
405
|
+
#
|
406
|
+
# @param cid [String]
|
407
|
+
# @param xnum [Integer] the index number
|
408
|
+
# @param lnum [Integer] the log number
|
409
|
+
# @raise [Error::NotFound] if it does not exist
|
410
|
+
# @raise [Error::Perms] if user does not have permissions
|
411
|
+
#
|
412
|
+
def index_read(cid, xnum, lnum=0)
|
413
|
+
|
414
|
+
# get access list
|
415
|
+
al = access_list(cid)
|
416
|
+
if !al.include?(ICFS::PermRead)
|
417
|
+
raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead )
|
418
|
+
end
|
419
|
+
|
420
|
+
# read curent index
|
421
|
+
json = @cache.index_read(cid, xnum)
|
422
|
+
xc = Validate.parse(json, 'index'.freeze, Items::ItemIndex)
|
423
|
+
|
424
|
+
# return the requested version
|
425
|
+
if( lnum == 0 || xc['log'] == lnum )
|
426
|
+
return xc
|
427
|
+
else
|
428
|
+
json = @store.index_read(cid, xnum, lnum)
|
429
|
+
return Validate.parse(json, 'index'.freeze, Items::ItemIndex)
|
430
|
+
end
|
431
|
+
end # def index_read()
|
432
|
+
|
433
|
+
|
434
|
+
###############################################
|
435
|
+
# Read a current
|
436
|
+
#
|
437
|
+
# @param cid [String] caseid
|
438
|
+
#
|
439
|
+
def current_read(cid)
|
440
|
+
|
441
|
+
al = access_list(cid)
|
442
|
+
if !al.include?(ICFS::PermRead)
|
443
|
+
raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead)
|
444
|
+
end
|
445
|
+
|
446
|
+
json = @cache.current_read(cid)
|
447
|
+
return Validate.parse(json, 'current'.current, Items::ItemCurrent)
|
448
|
+
end # end def current_read()
|
449
|
+
|
450
|
+
|
451
|
+
##############################################################
|
452
|
+
# Searches
|
453
|
+
##############################################################
|
454
|
+
|
455
|
+
|
456
|
+
# Validate a case search query
|
457
|
+
ValCaseSearch = {
|
458
|
+
method: :hash,
|
459
|
+
optional: {
|
460
|
+
title: Items::FieldTitle,
|
461
|
+
tags: Items::FieldTagAny,
|
462
|
+
status: Validate::IsBoolean,
|
463
|
+
template: Validate::IsBoolean,
|
464
|
+
grantee: Items::FieldUsergrp,
|
465
|
+
perm: Items::FieldPermAny,
|
466
|
+
size: ValSize,
|
467
|
+
page: ValPage,
|
468
|
+
purpose: ValPurpose,
|
469
|
+
}.freeze,
|
470
|
+
}.freeze
|
471
|
+
|
472
|
+
|
473
|
+
###############################################
|
474
|
+
# Search for a case
|
475
|
+
#
|
476
|
+
# @param query [Hash] a query
|
477
|
+
#
|
478
|
+
def case_search(query)
|
479
|
+
Validate.validate(query, 'Case Search'.freeze, ValCaseSearch)
|
480
|
+
@cache.case_search(query)
|
481
|
+
end
|
482
|
+
|
483
|
+
|
484
|
+
# Validate a log search
|
485
|
+
ValLogSearch = {
|
486
|
+
method: :hash,
|
487
|
+
optional: {
|
488
|
+
caseid: Items::FieldCaseid,
|
489
|
+
after: Validate::IsIntPos,
|
490
|
+
before: Validate::IsIntPos,
|
491
|
+
user: Items::FieldUsergrp,
|
492
|
+
entry: Validate::IsIntPos,
|
493
|
+
index: Validate::IsIntPos,
|
494
|
+
action: Validate::IsIntPos,
|
495
|
+
size: ValSize,
|
496
|
+
page: ValPage,
|
497
|
+
purpose: ValPurpose,
|
498
|
+
sort: {
|
499
|
+
method: :string,
|
500
|
+
allowed: Set[
|
501
|
+
'time_desc'.freeze,
|
502
|
+
'time_asc'.freeze,
|
503
|
+
].freeze,
|
504
|
+
whitelist: true,
|
505
|
+
}.freeze
|
506
|
+
}.freeze
|
507
|
+
}.freeze
|
508
|
+
|
509
|
+
|
510
|
+
###############################################
|
511
|
+
# Search for a log
|
512
|
+
#
|
513
|
+
# @param query [Hash] a query
|
514
|
+
#
|
515
|
+
def log_search(query)
|
516
|
+
Validate.validate(query, 'Log Search'.freeze, ValLogSearch)
|
517
|
+
@cache.log_search(query)
|
518
|
+
end
|
519
|
+
|
520
|
+
|
521
|
+
# Validate an entry search query
|
522
|
+
ValEntrySearch = {
|
523
|
+
method: :hash,
|
524
|
+
optional: {
|
525
|
+
title: Items::FieldTitle,
|
526
|
+
content: Items::FieldContent,
|
527
|
+
tags: Items::FieldTagAny,
|
528
|
+
caseid: Items::FieldCaseid,
|
529
|
+
action: Validate::IsIntPos,
|
530
|
+
index: Validate::IsIntPos,
|
531
|
+
after: Validate::IsIntPos,
|
532
|
+
before: Validate::IsIntPos,
|
533
|
+
stat: Items::FieldStat,
|
534
|
+
credit: Items::FieldUsergrp,
|
535
|
+
size: ValSize,
|
536
|
+
page: ValPage,
|
537
|
+
purpose: ValPurpose,
|
538
|
+
sort: {
|
539
|
+
method: :string,
|
540
|
+
allowed: Set[
|
541
|
+
'time_desc'.freeze,
|
542
|
+
'time_asc'.freeze,
|
543
|
+
].freeze,
|
544
|
+
whitelist: true,
|
545
|
+
}.freeze
|
546
|
+
}.freeze
|
547
|
+
}.freeze
|
548
|
+
|
549
|
+
|
550
|
+
|
551
|
+
###############################################
|
552
|
+
# Search for entries
|
553
|
+
#
|
554
|
+
def entry_search(query)
|
555
|
+
Validate.validate(query, 'Entry Search'.freeze, ValEntrySearch)
|
556
|
+
|
557
|
+
# check permissions
|
558
|
+
# - have global search permissions / read access to the case
|
559
|
+
# - are searching for an action they can read
|
560
|
+
unless( _search?(query) || (query[:caseid] &&
|
561
|
+
query[:action] && tasked?(query[:caseid], query[:action])))
|
562
|
+
raise(Error::Perms, 'Does not have permission to search'.freeze)
|
563
|
+
end
|
564
|
+
|
565
|
+
# run the query
|
566
|
+
res = @cache.entry_search(query)
|
567
|
+
|
568
|
+
# check perms for each entry
|
569
|
+
res[:list].each do |se|
|
570
|
+
ent = se[:object]
|
571
|
+
|
572
|
+
# can not read the case/action - basically nothing
|
573
|
+
unless _can_read?(ent[:caseid], ent[:action])
|
574
|
+
ent[:time] = nil
|
575
|
+
ent[:title] = nil
|
576
|
+
ent[:perms] = nil
|
577
|
+
ent[:action] = nil
|
578
|
+
ent[:tags] = nil
|
579
|
+
ent[:files] = nil
|
580
|
+
ent[:stats] = nil
|
581
|
+
se[:snippet] = nil
|
582
|
+
next
|
583
|
+
end
|
584
|
+
|
585
|
+
# can read the case/action, missing perms for this entry
|
586
|
+
# leave time, perms, and action
|
587
|
+
al = access_list(ent[:caseid])
|
588
|
+
if !(Set.new(ent[:perms]) - al).empty?
|
589
|
+
ent[:title] = nil
|
590
|
+
ent[:tags] = nil
|
591
|
+
ent[:files] = nil
|
592
|
+
ent[:stats] = nil
|
593
|
+
se[:snippet] = nil
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
return res
|
598
|
+
end # def entry_search()
|
599
|
+
|
600
|
+
|
601
|
+
# Validate a task search
|
602
|
+
ValActionSearch = {
|
603
|
+
method: :hash,
|
604
|
+
required: {
|
605
|
+
assigned: {
|
606
|
+
method: :any,
|
607
|
+
check: [
|
608
|
+
Items::FieldUsergrp,
|
609
|
+
{
|
610
|
+
method: :equals,
|
611
|
+
check: ICFS::UserCase
|
612
|
+
}
|
613
|
+
].freeze
|
614
|
+
}.freeze
|
615
|
+
}.freeze,
|
616
|
+
optional: {
|
617
|
+
caseid: Items::FieldCaseid,
|
618
|
+
title: Items::FieldTitle,
|
619
|
+
status: Validate::IsBoolean,
|
620
|
+
flag: Validate::IsBoolean,
|
621
|
+
before: Validate::IsIntPos,
|
622
|
+
after: Validate::IsIntPos,
|
623
|
+
tags: Items::FieldTagAny,
|
624
|
+
size: ValSize,
|
625
|
+
page: ValPage,
|
626
|
+
purpose: ValPurpose,
|
627
|
+
sort: {
|
628
|
+
method: :string,
|
629
|
+
allowed: Set[
|
630
|
+
'time_desc'.freeze,
|
631
|
+
'time_asc'.freeze,
|
632
|
+
].freeze,
|
633
|
+
whitelist: true,
|
634
|
+
}.freeze
|
635
|
+
}.freeze
|
636
|
+
}.freeze
|
637
|
+
|
638
|
+
|
639
|
+
###############################################
|
640
|
+
# Search for actions
|
641
|
+
#
|
642
|
+
def action_search(query)
|
643
|
+
Validate.validate(query, 'Action Search'.freeze, ValActionSearch)
|
644
|
+
|
645
|
+
# permissions check
|
646
|
+
# - have global search permissions / read access to the case
|
647
|
+
# - searching for role you have
|
648
|
+
unless( _search?(query) || @ur.include?(query[:assigned]) ||
|
649
|
+
(query[:assigned] == ICFS::UserCase && query[:caseid] &&
|
650
|
+
access_list(query[:caseid]).include?(ICFS::PermAction) ))
|
651
|
+
raise(Error::Perms, 'Does not have permission to search'.freeze)
|
652
|
+
end
|
653
|
+
|
654
|
+
# run the search
|
655
|
+
return @cache.action_search(query)
|
656
|
+
end # def action_search()
|
657
|
+
|
658
|
+
|
659
|
+
# Validate an index search query
|
660
|
+
ValIndexSearch = {
|
661
|
+
method: :hash,
|
662
|
+
optional: {
|
663
|
+
caseid: Items::FieldCaseid,
|
664
|
+
title: Items::FieldTitle,
|
665
|
+
prefix: Items::FieldTitle,
|
666
|
+
content: Items::FieldContent,
|
667
|
+
tags: Items::FieldTagAny,
|
668
|
+
size: ValSize,
|
669
|
+
page: ValPage,
|
670
|
+
purpose: ValPurpose,
|
671
|
+
sort: {
|
672
|
+
method: :string,
|
673
|
+
allowed: Set[
|
674
|
+
'title_desc'.freeze,
|
675
|
+
'title_asc'.freeze,
|
676
|
+
'index_desc'.freeze,
|
677
|
+
'index_asc'.freeze,
|
678
|
+
].freeze,
|
679
|
+
whitelist: true,
|
680
|
+
}.freeze
|
681
|
+
}.freeze
|
682
|
+
}.freeze
|
683
|
+
|
684
|
+
|
685
|
+
###############################################
|
686
|
+
# Search for indexes
|
687
|
+
#
|
688
|
+
def index_search(query)
|
689
|
+
Validate.validate(query, 'Index Search'.freeze, ValIndexSearch)
|
690
|
+
|
691
|
+
# permissions check
|
692
|
+
# - have global search permissions / read access to the case
|
693
|
+
unless _search?(query)
|
694
|
+
raise(Error::Perms, 'Do not have permission to search'.freeze)
|
695
|
+
end
|
696
|
+
|
697
|
+
# run the query
|
698
|
+
res = @cache.index_search(query)
|
699
|
+
|
700
|
+
# check perms for each index
|
701
|
+
res[:list].each do |se|
|
702
|
+
idx = se[:object]
|
703
|
+
|
704
|
+
unless access_list(idx[:caseid].include?(ICFS::PermRead))
|
705
|
+
idx[:title] = nil
|
706
|
+
idx[:tags] = nil
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
return res
|
711
|
+
end
|
712
|
+
|
713
|
+
|
714
|
+
# validate the stats query
|
715
|
+
ValStatsSearch = {
|
716
|
+
method: :hash,
|
717
|
+
optional: {
|
718
|
+
caseid: Items::FieldCaseid,
|
719
|
+
after: Validate::IsIntPos,
|
720
|
+
before: Validate::IsIntPos,
|
721
|
+
credit: Items::FieldUsergrp,
|
722
|
+
purpose: ValPurpose,
|
723
|
+
}.freeze
|
724
|
+
}.freeze
|
725
|
+
|
726
|
+
|
727
|
+
###############################################
|
728
|
+
# Analyze stats
|
729
|
+
#
|
730
|
+
def stats(query)
|
731
|
+
Validate.validate(query, 'Stats Search'.freeze, ValStatsSearch)
|
732
|
+
|
733
|
+
# permissions check
|
734
|
+
# - have global search permissions / read access to the case
|
735
|
+
# - are searching for a user/role/group you have
|
736
|
+
unless _search?(query) || (query[:credit] && @urg.include?(query[:credit]))
|
737
|
+
raise(Error::Perms, 'Do not have permissions to search'.freeze)
|
738
|
+
end
|
739
|
+
|
740
|
+
@cache.stats(query)
|
741
|
+
end
|
742
|
+
|
743
|
+
|
744
|
+
# Case Tags search validation
|
745
|
+
ValCaseTags = {
|
746
|
+
method: :hash,
|
747
|
+
optional: {
|
748
|
+
status: Validate::IsBoolean,
|
749
|
+
template: Validate::IsBoolean,
|
750
|
+
grantee: Items::FieldUsergrp,
|
751
|
+
purpose: ValPurpose,
|
752
|
+
}.freeze,
|
753
|
+
}.freeze
|
754
|
+
|
755
|
+
|
756
|
+
###############################################
|
757
|
+
# Get case tags
|
758
|
+
#
|
759
|
+
def case_tags(query)
|
760
|
+
Validate.validate(query, 'Case Tags Search'.freeze, ValCaseTags)
|
761
|
+
return @cache.case_tags(query)
|
762
|
+
end # def case_tags()
|
763
|
+
|
764
|
+
|
765
|
+
# Entry Tags search validation
|
766
|
+
ValEntryTags = {
|
767
|
+
method: :hash,
|
768
|
+
required: {
|
769
|
+
caseid: Items::FieldCaseid,
|
770
|
+
}.freeze,
|
771
|
+
optional: {
|
772
|
+
purpose: ValPurpose,
|
773
|
+
}.freeze,
|
774
|
+
}.freeze
|
775
|
+
|
776
|
+
|
777
|
+
###############################################
|
778
|
+
# Get entry tags
|
779
|
+
#
|
780
|
+
def entry_tags(query)
|
781
|
+
Validate.validate(query, 'Entry Tags Search'.freeze, ValEntryTags)
|
782
|
+
|
783
|
+
# permissions
|
784
|
+
# - read access to case
|
785
|
+
unless access_list(query[:caseid]).include?(ICFS::PermRead)
|
786
|
+
raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead)
|
787
|
+
end
|
788
|
+
return @cache.entry_tags(query)
|
789
|
+
end # def entry_tags()
|
790
|
+
|
791
|
+
|
792
|
+
# Task Tags search validation
|
793
|
+
ValActionTags = {
|
794
|
+
method: :hash,
|
795
|
+
required: {
|
796
|
+
assigned: {
|
797
|
+
method: :any,
|
798
|
+
check: [
|
799
|
+
Items::FieldUsergrp,
|
800
|
+
{
|
801
|
+
method: :equals,
|
802
|
+
check: ICFS::UserCase
|
803
|
+
}.freeze
|
804
|
+
].freeze
|
805
|
+
}.freeze,
|
806
|
+
}.freeze,
|
807
|
+
optional: {
|
808
|
+
caseid: Items::FieldCaseid,
|
809
|
+
status: Validate::IsBoolean,
|
810
|
+
flag: Validate::IsBoolean,
|
811
|
+
before: Validate::IsIntPos,
|
812
|
+
after: Validate::IsIntPos,
|
813
|
+
purpose: ValPurpose,
|
814
|
+
}.freeze,
|
815
|
+
}.freeze
|
816
|
+
|
817
|
+
|
818
|
+
###############################################
|
819
|
+
# Get action tags
|
820
|
+
#
|
821
|
+
def action_tags(query)
|
822
|
+
Validate.validate(query, 'Task Tags Search'.freeze, ValActionTags)
|
823
|
+
|
824
|
+
# only allow searches for user/roles you have
|
825
|
+
unless @ur.include?(query[:assigned]) ||
|
826
|
+
(query[:assigned] == ICFS::UserCase && query[:caseid] &&
|
827
|
+
access_list(query[:caseid]).include?(ICFS::PermAction) )
|
828
|
+
raise(Error::Perms, 'May not search for other\'s tasks'.freeze)
|
829
|
+
end
|
830
|
+
|
831
|
+
# run the search
|
832
|
+
return @cache.action_tags(query)
|
833
|
+
end # def action_tags()
|
834
|
+
|
835
|
+
|
836
|
+
# Validate a index tag search
|
837
|
+
ValIndexTags = {
|
838
|
+
method: :hash,
|
839
|
+
required: {
|
840
|
+
caseid: Items::FieldCaseid,
|
841
|
+
}.freeze,
|
842
|
+
optional: {
|
843
|
+
purpose: ValPurpose,
|
844
|
+
}.freeze
|
845
|
+
}.freeze
|
846
|
+
|
847
|
+
|
848
|
+
###############################################
|
849
|
+
# Get index tags
|
850
|
+
#
|
851
|
+
def index_tags(query)
|
852
|
+
Validate.validate(query, 'Index Tags'.freeze, ValIndexTags)
|
853
|
+
unless access_list(query[:caseid]).include?(ICFS::PermRead)
|
854
|
+
raise(Error::Perms, 'missing perms: %s'.freeze % ICFS::PermRead)
|
855
|
+
end
|
856
|
+
return @cache.index_tags(query)
|
857
|
+
end
|
858
|
+
|
859
|
+
|
860
|
+
##############################################################
|
861
|
+
# Record
|
862
|
+
##############################################################
|
863
|
+
|
864
|
+
|
865
|
+
###############################################
|
866
|
+
# Create a new case
|
867
|
+
#
|
868
|
+
# @param ent [Hash] the first entry
|
869
|
+
# @param cse [Hash] the case
|
870
|
+
# @param tid [String] the template name
|
871
|
+
#
|
872
|
+
def case_create(ent, cse, tid=nil)
|
873
|
+
|
874
|
+
####################
|
875
|
+
# Sanity checks
|
876
|
+
|
877
|
+
# form & values
|
878
|
+
Validate.validate(ent, 'entry'.freeze, Items::ItemEntryNew)
|
879
|
+
Validate.validate(cse, 'case'.freeze, Items::ItemCaseEdit)
|
880
|
+
|
881
|
+
# access users/roles/groups are valid
|
882
|
+
cse["access"].each do |acc|
|
883
|
+
acc["grant"].each do |gnt|
|
884
|
+
urg = @users.read(gnt)
|
885
|
+
if !urg
|
886
|
+
raise(Error::NotFound, 'User/role/group %s not found'.freeze % urg)
|
887
|
+
end
|
888
|
+
end
|
889
|
+
end
|
890
|
+
|
891
|
+
# permissions
|
892
|
+
perms = Set[ ICFS::PermManage ]
|
893
|
+
perms.merge(ent['perms']) if ent['perms']
|
894
|
+
|
895
|
+
# template
|
896
|
+
if tid
|
897
|
+
tmpl = case_read(tid)
|
898
|
+
unless tmpl['template']
|
899
|
+
raise(Error::Perms, 'Not a template'.freeze)
|
900
|
+
end
|
901
|
+
|
902
|
+
al = access_list(tid)
|
903
|
+
unless al.include?(ICFS::PermManage)
|
904
|
+
raise(Error::Perms, 'May not create cases from this template'.freeze)
|
905
|
+
end
|
906
|
+
end
|
907
|
+
|
908
|
+
# no action/indexes
|
909
|
+
if ent['action']
|
910
|
+
raise(Error::Value, 'No Action for a new case entry'.freeze)
|
911
|
+
end
|
912
|
+
if ent['index']
|
913
|
+
raise(Error::Value, 'No Index for a new case entry'.freeze)
|
914
|
+
end
|
915
|
+
|
916
|
+
|
917
|
+
####################
|
918
|
+
# Prep
|
919
|
+
|
920
|
+
# case
|
921
|
+
cid = ent['caseid']
|
922
|
+
cse['icfs'] = 1
|
923
|
+
cse['caseid'] = cid
|
924
|
+
cse['log'] = 1
|
925
|
+
cse['tags'] ||= [ ICFS::TagNone ]
|
926
|
+
citem = Validate.generate(cse, 'case'.freeze, Items::ItemCase)
|
927
|
+
|
928
|
+
# entry
|
929
|
+
ent['icfs'] = 1
|
930
|
+
ent['entry'] = 1
|
931
|
+
ent['log'] = 1
|
932
|
+
ent['tags'] ||= [ ]
|
933
|
+
ent['tags'] << ICFS::TagCase
|
934
|
+
ent['user'] = @user
|
935
|
+
files, fhash = _pre_files(ent)
|
936
|
+
|
937
|
+
# log
|
938
|
+
log = {
|
939
|
+
'icfs' => 1,
|
940
|
+
'caseid' => cid,
|
941
|
+
'log' => 1,
|
942
|
+
'prev' => '0'*64,
|
943
|
+
'user' => @user,
|
944
|
+
'entry' => {
|
945
|
+
'num' => 1,
|
946
|
+
},
|
947
|
+
'case_hash' => ICFS.hash(citem),
|
948
|
+
}
|
949
|
+
log['files_hash'] = fhash if fhash
|
950
|
+
|
951
|
+
# current
|
952
|
+
cur = {
|
953
|
+
'icfs' => 1,
|
954
|
+
'caseid' => cid,
|
955
|
+
'log' => 1,
|
956
|
+
'entry' => 1,
|
957
|
+
'action' => 0,
|
958
|
+
'index' => 0
|
959
|
+
}
|
960
|
+
|
961
|
+
####################
|
962
|
+
# Write the case
|
963
|
+
|
964
|
+
# take lock
|
965
|
+
@cache.lock_take(cid)
|
966
|
+
begin
|
967
|
+
if @cache.case_read(cid)
|
968
|
+
raise(Error::Conflict, 'Case already exists'.freeze)
|
969
|
+
end
|
970
|
+
|
971
|
+
now = Time.now.to_i
|
972
|
+
|
973
|
+
# finish items
|
974
|
+
ent['time'] ||= now
|
975
|
+
ent['files'].each{|fi| fi['log'] ||= 1 } if ent['files']
|
976
|
+
eitem = Validate.generate(ent, 'entry'.freeze, Items::ItemEntry)
|
977
|
+
log['time'] = now
|
978
|
+
log['entry']['hash'] = ICFS.hash(eitem)
|
979
|
+
litem = Validate.generate(log, 'log'.freeze, Items::ItemLog)
|
980
|
+
cur['hash'] = ICFS.hash(litem)
|
981
|
+
nitem = Validate.generate(cur, 'current'.freeze, Items::ItemCurrent)
|
982
|
+
|
983
|
+
# write to cache
|
984
|
+
@cache.entry_write(cid, 1, eitem)
|
985
|
+
@cache.log_write(cid, 1, litem)
|
986
|
+
@cache.case_write(cid, citem)
|
987
|
+
@cache.current_write(cid, nitem)
|
988
|
+
|
989
|
+
# write to store
|
990
|
+
@store.entry_write(cid, 1, 1, eitem)
|
991
|
+
@store.log_write(cid, 1, litem)
|
992
|
+
@store.case_write(cid, 1, citem)
|
993
|
+
|
994
|
+
# release lock
|
995
|
+
ensure
|
996
|
+
@cache.lock_release(cid)
|
997
|
+
end
|
998
|
+
|
999
|
+
# files
|
1000
|
+
files.each_index{|ix| @store.file_write(cid, 1, 1, ix+1, files[ix]) }
|
1001
|
+
|
1002
|
+
end # def case_create()
|
1003
|
+
|
1004
|
+
|
1005
|
+
###############################################
|
1006
|
+
# Write items to a case
|
1007
|
+
#
|
1008
|
+
# @param ent [Hash] Entry to record, required
|
1009
|
+
# @param act [Hash, Nilclass] Action to record, optional
|
1010
|
+
# @param idx [Hash, Nilclass] Index to record, optional
|
1011
|
+
# @param cse [Hash, Nilclass] Case to record, optional
|
1012
|
+
#
|
1013
|
+
def record(ent, act, idx, cse)
|
1014
|
+
|
1015
|
+
####################
|
1016
|
+
# Sanity checks
|
1017
|
+
|
1018
|
+
# form & content
|
1019
|
+
if idx || cse
|
1020
|
+
Validate.validate(ent, 'New Entry'.freeze, Items::ItemEntryNew)
|
1021
|
+
else
|
1022
|
+
Validate.validate(ent, 'Editable Entry'.freeze, Items::ItemEntryEdit)
|
1023
|
+
end
|
1024
|
+
Validate.validate(act, 'action'.freeze, Items::ItemActionEdit) if act
|
1025
|
+
Validate.validate(idx, 'index'.freeze, Items::ItemIndexEdit) if idx
|
1026
|
+
Validate.validate(cse, 'case'.freeze, Items::ItemCaseEdit) if cse
|
1027
|
+
|
1028
|
+
# edit index OR case, not both
|
1029
|
+
if idx && cse
|
1030
|
+
raise(Error::Value, 'May not edit both case and index at once'.freeze)
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
# no changing the action
|
1034
|
+
if act && ent['action'] && act['action'] && act['action'] != ent['action']
|
1035
|
+
raise(Error::Conflict, 'May not change entry\'s action'.freeze)
|
1036
|
+
end
|
1037
|
+
|
1038
|
+
# access users/roles/groups are valid
|
1039
|
+
if cse
|
1040
|
+
cse['access'].each do |acc|
|
1041
|
+
acc['grant'].each do |gnt|
|
1042
|
+
urg = @users.read(gnt)
|
1043
|
+
if !urg
|
1044
|
+
raise(Error::NotFound, 'User/role/group %s not found'.freeze % gnt)
|
1045
|
+
end
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
# tasking users/roles are valid
|
1051
|
+
if act
|
1052
|
+
act['tasks'].each_index do |ix|
|
1053
|
+
next if ix == 0
|
1054
|
+
tsk = act['tasks'][ix]
|
1055
|
+
ur = @users.read(tsk['assigned'])
|
1056
|
+
if !ur
|
1057
|
+
raise(Error::NotFound, 'User/role %s not found'.freeze %
|
1058
|
+
tsk['assigned'])
|
1059
|
+
end
|
1060
|
+
type = ur['type']
|
1061
|
+
if type != 'user' && type != 'role'
|
1062
|
+
raise(Error::Values, 'Not a user or role: %s'.freeze %
|
1063
|
+
tsk['assigned'])
|
1064
|
+
end
|
1065
|
+
end
|
1066
|
+
end
|
1067
|
+
|
1068
|
+
|
1069
|
+
####################
|
1070
|
+
# Prep
|
1071
|
+
cid = ent['caseid']
|
1072
|
+
|
1073
|
+
# entry
|
1074
|
+
ent['icfs'] = 1
|
1075
|
+
ent['tags'] ||= [ ]
|
1076
|
+
ent['user'] = @user
|
1077
|
+
files, fhash = _pre_files(ent)
|
1078
|
+
|
1079
|
+
# action
|
1080
|
+
if act
|
1081
|
+
ent['tags'] << ICFS::TagAction
|
1082
|
+
act['icfs'] = 1
|
1083
|
+
act['caseid'] = cid
|
1084
|
+
act['tasks'].each do |tk|
|
1085
|
+
tk['tags'] ||= [ ICFS::TagNone ]
|
1086
|
+
end
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
# index
|
1090
|
+
if idx
|
1091
|
+
ent['tags'] << ICFS::TagIndex
|
1092
|
+
idx['icfs'] = 1
|
1093
|
+
idx['caseid'] = cid
|
1094
|
+
idx['tags'] ||= [ ICFS::TagNone ]
|
1095
|
+
end
|
1096
|
+
|
1097
|
+
# case
|
1098
|
+
if cse
|
1099
|
+
ent['tags'] << ICFS::TagCase
|
1100
|
+
cse['icfs'] = 1
|
1101
|
+
cse['caseid'] = cid
|
1102
|
+
cse['tags'] ||= [ ICFS::TagNone ]
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
# log
|
1106
|
+
log = {
|
1107
|
+
'icfs' => 1,
|
1108
|
+
'caseid' => cid,
|
1109
|
+
'user' => @user,
|
1110
|
+
}
|
1111
|
+
log['files_hash'] = fhash if fhash
|
1112
|
+
|
1113
|
+
# no tags
|
1114
|
+
ent['tags'] = [ ICFS::TagNone ] if ent['tags'].empty?
|
1115
|
+
|
1116
|
+
# current
|
1117
|
+
nxt = {
|
1118
|
+
'icfs' => 1,
|
1119
|
+
'caseid' => cid,
|
1120
|
+
}
|
1121
|
+
|
1122
|
+
|
1123
|
+
####################
|
1124
|
+
# Write
|
1125
|
+
|
1126
|
+
# take lock
|
1127
|
+
@cache.lock_take(cid)
|
1128
|
+
begin
|
1129
|
+
now = Time.now.to_i
|
1130
|
+
|
1131
|
+
####################
|
1132
|
+
# get prior items & numbers
|
1133
|
+
|
1134
|
+
# current
|
1135
|
+
json = @cache.current_read(cid)
|
1136
|
+
cur = Validate.parse(json, 'current'.freeze, Items::ItemCurrent)
|
1137
|
+
|
1138
|
+
# entry
|
1139
|
+
if ent['entry']
|
1140
|
+
enum = ent['entry']
|
1141
|
+
json = @cache.entry_read(cid, enum)
|
1142
|
+
ent_pri = Validate.parse(json, 'entry'.freeze, Items::ItemEntry)
|
1143
|
+
nxt['entry'] = cur['entry']
|
1144
|
+
else
|
1145
|
+
enum = cur['entry'] + 1
|
1146
|
+
nxt['entry'] = enum
|
1147
|
+
end
|
1148
|
+
|
1149
|
+
# action
|
1150
|
+
if ent_pri && ent_pri['action']
|
1151
|
+
anum = ent_pri['action']
|
1152
|
+
elsif act && act['action']
|
1153
|
+
anum = act['action']
|
1154
|
+
elsif ent['action']
|
1155
|
+
anum = ent['action']
|
1156
|
+
end
|
1157
|
+
if anum
|
1158
|
+
json = @cache.action_read(cid, anum)
|
1159
|
+
act_pri = Validate.parse(json, 'action'.freeze, Items::ItemAction)
|
1160
|
+
nxt['action'] = cur['action']
|
1161
|
+
elsif act
|
1162
|
+
anum = cur['action'] + 1
|
1163
|
+
nxt['action'] = anum
|
1164
|
+
else
|
1165
|
+
nxt['action'] = cur['action']
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
# index
|
1169
|
+
if idx
|
1170
|
+
if idx['index']
|
1171
|
+
xnum = idx['index']
|
1172
|
+
nxt['index'] = cur['index']
|
1173
|
+
else
|
1174
|
+
xnum = cur['index'] + 1
|
1175
|
+
nxt['index'] = xnum
|
1176
|
+
end
|
1177
|
+
else
|
1178
|
+
xnum = nil
|
1179
|
+
nxt['index'] = cur['index']
|
1180
|
+
end
|
1181
|
+
|
1182
|
+
# case
|
1183
|
+
cse_pri = case_read(cid)
|
1184
|
+
al = access_list(cid)
|
1185
|
+
|
1186
|
+
# log
|
1187
|
+
lnum = cur['log'] + 1
|
1188
|
+
nxt['log'] = lnum
|
1189
|
+
|
1190
|
+
|
1191
|
+
####################
|
1192
|
+
# Checks
|
1193
|
+
perms = Set.new
|
1194
|
+
|
1195
|
+
# entry
|
1196
|
+
perms.merge(ent['perms']) if ent['perms']
|
1197
|
+
if ent_pri
|
1198
|
+
|
1199
|
+
# must have those perms
|
1200
|
+
perms.add(ent_pri['perms']) if ent_pri['perms']
|
1201
|
+
|
1202
|
+
# may not change action
|
1203
|
+
if ent_pri['action'] && (ent['action'] != ent_pri['action'])
|
1204
|
+
raise(Error::Conflict, 'May not change entry\'s action'.freeze)
|
1205
|
+
end
|
1206
|
+
|
1207
|
+
# may not remove or add action, index, case tags
|
1208
|
+
if( (ent_pri['tags'].include?(ICFS::TagAction) !=
|
1209
|
+
ent['tags'].include?(ICFS::TagAction) ) ||
|
1210
|
+
(ent_pri['tags'].include?(ICFS::TagIndex) !=
|
1211
|
+
ent['tags'].include?(ICFS::TagIndex) ) ||
|
1212
|
+
(ent_pri['tags'].include?(ICFS::TagCase) !=
|
1213
|
+
ent['tags'].include?(ICFS::TagCase) ) )
|
1214
|
+
raise(Error::Conflict, 'May not change entry\'s special tags'.freeze)
|
1215
|
+
end
|
1216
|
+
end
|
1217
|
+
|
1218
|
+
# action
|
1219
|
+
if act
|
1220
|
+
pri_tsk = act_pri ? act_pri['tasks'] : []
|
1221
|
+
cur_tsk = act['tasks']
|
1222
|
+
act_open = cur_tsk[0]['status']
|
1223
|
+
|
1224
|
+
# not allowed to delete tasks
|
1225
|
+
if pri_tsk.size > cur_tsk.size
|
1226
|
+
raise(Error::Conflict, 'May not delete tasks'.freeze)
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
# check each task
|
1230
|
+
perm_act = al.include?(ICFS::PermAction)
|
1231
|
+
tasked = false
|
1232
|
+
cur_tsk.each_index do |ix|
|
1233
|
+
ct = cur_tsk[ix]
|
1234
|
+
pt = pri_tsk[ix]
|
1235
|
+
|
1236
|
+
# may not delete a tasking
|
1237
|
+
if pt && pt['assigned'] != ct['assigned']
|
1238
|
+
raise(Error::Conflict, 'May not delete task'.freeze)
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
# new taskings require action to be open
|
1242
|
+
if !pt && !act_open
|
1243
|
+
raise(Error::Value, 'New tasks require the action be open'.freeze)
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
# may not have a task open if action is closed
|
1247
|
+
if ct['status'] && !act_open
|
1248
|
+
raise(Error::Value, 'Open tasks on closed action'.freeze)
|
1249
|
+
end
|
1250
|
+
|
1251
|
+
# can set any values for our tasks
|
1252
|
+
if @ur.include?(ct['assigned']) || (ix == 0 && perm_act )
|
1253
|
+
tasked = true
|
1254
|
+
next
|
1255
|
+
end
|
1256
|
+
|
1257
|
+
# must be flagged if new tasking or re-opening
|
1258
|
+
if !ct['flag'] && (!pt || (ct['status'] && !pt['status']))
|
1259
|
+
raise(Error::Value, 'New or re-opened taskings must flag'.freeze)
|
1260
|
+
end
|
1261
|
+
|
1262
|
+
# no changing other's taskings, no deflagging, and no
|
1263
|
+
# closing task without action
|
1264
|
+
if pt && (
|
1265
|
+
(pt['title'] != ct['title']) || (pt['time'] != ct['time']) ||
|
1266
|
+
(pt['tags'] != ct['tags']) || (pt['flag'] && !ct['flag']) ||
|
1267
|
+
(pt['status'] && !ct['status'] && !perm_act) )
|
1268
|
+
raise(Error::Value, 'May not change other\'s tasks'.freeze)
|
1269
|
+
end
|
1270
|
+
end
|
1271
|
+
|
1272
|
+
# new tasks or changes to other's tasks
|
1273
|
+
if !act_pri || !tasked
|
1274
|
+
perms.add( ICFS::PermAction )
|
1275
|
+
end
|
1276
|
+
|
1277
|
+
end
|
1278
|
+
|
1279
|
+
# no checks for index
|
1280
|
+
|
1281
|
+
# case
|
1282
|
+
if cse
|
1283
|
+
# no changing template
|
1284
|
+
unless cse['template'] == cse_pri['template']
|
1285
|
+
raise(Error::Conflict, 'May not change template status'.freeze)
|
1286
|
+
end
|
1287
|
+
|
1288
|
+
# manage required
|
1289
|
+
perms.add( ICFS::PermManage ) if cse
|
1290
|
+
end
|
1291
|
+
|
1292
|
+
# write unless a case or pre-existing action
|
1293
|
+
unless cse || act_pri
|
1294
|
+
perms.add( ICFS::PermWrite)
|
1295
|
+
end
|
1296
|
+
|
1297
|
+
# permissions
|
1298
|
+
perms_miss = perms - al
|
1299
|
+
unless perms_miss.empty?
|
1300
|
+
raise(Error::Perms, 'Missing perms: %s'.freeze %
|
1301
|
+
perms_miss.to_a.sort.join(', ') )
|
1302
|
+
end
|
1303
|
+
|
1304
|
+
|
1305
|
+
####################
|
1306
|
+
# Items
|
1307
|
+
|
1308
|
+
# entry
|
1309
|
+
ent['entry'] = enum
|
1310
|
+
ent['log'] = lnum
|
1311
|
+
ent['time'] ||= now
|
1312
|
+
ent['action'] = anum if act
|
1313
|
+
if idx
|
1314
|
+
if ent['index']
|
1315
|
+
ent['index'] = ent['index'].push(xnum).uniq.sort
|
1316
|
+
else
|
1317
|
+
ent['index'] = [ xnum ]
|
1318
|
+
end
|
1319
|
+
end
|
1320
|
+
ent['files'].each{|fi| fi['log'] ||= lnum } if ent['files']
|
1321
|
+
eitem = Validate.generate(ent, 'entry'.freeze, Items::ItemEntry)
|
1322
|
+
log['entry'] = {
|
1323
|
+
'num' => enum,
|
1324
|
+
'hash' => ICFS.hash(eitem)
|
1325
|
+
}
|
1326
|
+
|
1327
|
+
# action
|
1328
|
+
if act
|
1329
|
+
act['action'] = anum
|
1330
|
+
act['log'] = lnum
|
1331
|
+
aitem = Validate.generate(act, 'action'.freeze, Items::ItemAction)
|
1332
|
+
log['action'] = {
|
1333
|
+
'num' => anum,
|
1334
|
+
'hash' => ICFS.hash(aitem)
|
1335
|
+
}
|
1336
|
+
end
|
1337
|
+
|
1338
|
+
# index
|
1339
|
+
if idx
|
1340
|
+
idx['index'] = xnum
|
1341
|
+
idx['log'] = lnum
|
1342
|
+
xitem = Validate.generate(idx, 'index'.freeze, Items::ItemIndex)
|
1343
|
+
log['index'] = {
|
1344
|
+
'num' => xnum,
|
1345
|
+
'hash' => ICFS.hash(xitem)
|
1346
|
+
}
|
1347
|
+
end
|
1348
|
+
|
1349
|
+
# case
|
1350
|
+
if cse
|
1351
|
+
cse['log'] = lnum
|
1352
|
+
citem = Validate.generate(cse, 'case'.freeze, Items::ItemCase)
|
1353
|
+
log['case_hash'] = ICFS.hash(citem)
|
1354
|
+
end
|
1355
|
+
|
1356
|
+
# log
|
1357
|
+
log['log'] = lnum
|
1358
|
+
log['prev'] = cur['hash']
|
1359
|
+
log['time'] = now
|
1360
|
+
litem = Validate.generate(log, 'log'.freeze, Items::ItemLog)
|
1361
|
+
nxt['hash'] = ICFS.hash(litem)
|
1362
|
+
|
1363
|
+
# next
|
1364
|
+
nitem = Validate.generate(nxt, 'current'.freeze, Items::ItemCurrent)
|
1365
|
+
|
1366
|
+
|
1367
|
+
####################
|
1368
|
+
# Write
|
1369
|
+
|
1370
|
+
# entry
|
1371
|
+
@cache.entry_write(cid, enum, eitem)
|
1372
|
+
@store.entry_write(cid, enum, lnum, eitem)
|
1373
|
+
|
1374
|
+
# action
|
1375
|
+
if act
|
1376
|
+
@cache.action_write(cid, anum, aitem)
|
1377
|
+
@store.action_write(cid, anum, lnum, aitem)
|
1378
|
+
end
|
1379
|
+
|
1380
|
+
# index
|
1381
|
+
if idx
|
1382
|
+
@cache.index_write(cid, xnum, xitem)
|
1383
|
+
@store.index_write(cid, xnum, lnum, xitem)
|
1384
|
+
end
|
1385
|
+
|
1386
|
+
# case
|
1387
|
+
if cse
|
1388
|
+
@cache.case_write(cid, citem)
|
1389
|
+
@store.case_write(cid, lnum, citem)
|
1390
|
+
end
|
1391
|
+
|
1392
|
+
# log
|
1393
|
+
@cache.log_write(cid, lnum, litem)
|
1394
|
+
@store.log_write(cid, lnum, litem)
|
1395
|
+
|
1396
|
+
# current
|
1397
|
+
@cache.current_write(cid, nitem)
|
1398
|
+
|
1399
|
+
# release the lock
|
1400
|
+
ensure
|
1401
|
+
@cache.lock_release(cid)
|
1402
|
+
end
|
1403
|
+
|
1404
|
+
# write the files
|
1405
|
+
files.each_index{|ix| @store.file_write(cid, enum, lnum, ix+1, files[ix]) }
|
1406
|
+
|
1407
|
+
end # def record()
|
1408
|
+
|
1409
|
+
|
1410
|
+
###############################################
|
1411
|
+
# Assemble files before taking the lock
|
1412
|
+
#
|
1413
|
+
def _pre_files(ent)
|
1414
|
+
|
1415
|
+
files = []
|
1416
|
+
if ent.key?('files')
|
1417
|
+
fhash = []
|
1418
|
+
ent['files'].each do |at|
|
1419
|
+
if at.key?('temp')
|
1420
|
+
fi = at['temp']
|
1421
|
+
at.delete('temp')
|
1422
|
+
files << fi
|
1423
|
+
at['num'] = files.size
|
1424
|
+
fhash << ICFS.hash_temp(fi)
|
1425
|
+
end
|
1426
|
+
end
|
1427
|
+
end
|
1428
|
+
|
1429
|
+
return [files, fhash]
|
1430
|
+
end # def _pre_files()
|
1431
|
+
private :_pre_files
|
1432
|
+
|
1433
|
+
|
1434
|
+
end # class ICFS::Api
|
1435
|
+
|
1436
|
+
end # module ICFS
|