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/users.rb
ADDED
@@ -0,0 +1,80 @@
|
|
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 'set'
|
13
|
+
|
14
|
+
module ICFS
|
15
|
+
|
16
|
+
##########################################################################
|
17
|
+
# User, Role, Group, and Global Perms
|
18
|
+
#
|
19
|
+
#
|
20
|
+
# @abstract
|
21
|
+
#
|
22
|
+
class Users
|
23
|
+
|
24
|
+
###############################################
|
25
|
+
# Validate a user
|
26
|
+
#
|
27
|
+
ValUser = {
|
28
|
+
method: :hash,
|
29
|
+
required: {
|
30
|
+
'name' => Items::FieldUsergrp,
|
31
|
+
'type' => {
|
32
|
+
method: :string,
|
33
|
+
allowed: Set[
|
34
|
+
'user'.freeze,
|
35
|
+
'role'.freeze,
|
36
|
+
'group'.freeze,
|
37
|
+
].freeze
|
38
|
+
}.freeze
|
39
|
+
}.freeze,
|
40
|
+
optional: {
|
41
|
+
'roles' => {
|
42
|
+
method: :array,
|
43
|
+
check: Items::FieldUsergrp,
|
44
|
+
uniq: true
|
45
|
+
}.freeze,
|
46
|
+
'groups' => {
|
47
|
+
method: :array,
|
48
|
+
check: Items::FieldUsergrp,
|
49
|
+
uniq: true
|
50
|
+
}.freeze,
|
51
|
+
'perms' => {
|
52
|
+
method: :array,
|
53
|
+
check: Items::FieldPermGlobal,
|
54
|
+
uniq: true
|
55
|
+
}.freeze
|
56
|
+
}.freeze
|
57
|
+
}.freeze
|
58
|
+
|
59
|
+
|
60
|
+
###############################################
|
61
|
+
# Read a user/role/group
|
62
|
+
#
|
63
|
+
# @param urg [String] User/Role/Group name
|
64
|
+
# @return [Hash] Will include :type and, if a user :roles, :groups, :perms
|
65
|
+
#
|
66
|
+
def read(urg); raise NotImplementedError; end
|
67
|
+
|
68
|
+
|
69
|
+
###############################################
|
70
|
+
# Write a user/role/group
|
71
|
+
#
|
72
|
+
# @param obj [Hash] Will include :name, :type, and if a user
|
73
|
+
# :roles, :groups, :perms
|
74
|
+
#
|
75
|
+
def write(obj); raise NotImplementedError; end
|
76
|
+
|
77
|
+
|
78
|
+
end # class ICFS::Users
|
79
|
+
|
80
|
+
end # module ICFS
|
@@ -0,0 +1,166 @@
|
|
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 'set'
|
13
|
+
require_relative 'elastic'
|
14
|
+
|
15
|
+
module ICFS
|
16
|
+
|
17
|
+
##########################################################################
|
18
|
+
# Implements {ICFS::Users Users} using Elasticsearch to cache
|
19
|
+
# details from another {ICFS::Users Users} instance.
|
20
|
+
#
|
21
|
+
class UsersElastic < Users
|
22
|
+
|
23
|
+
include Elastic
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
###############################################
|
28
|
+
# The ES mappings for the indexes
|
29
|
+
#
|
30
|
+
Maps = {
|
31
|
+
:users => '{
|
32
|
+
"mappings": { "_doc": { "properties": {
|
33
|
+
"name": { "type": "text" },
|
34
|
+
"type": { "type": "keyword" },
|
35
|
+
"roles": { "type": "keyword" },
|
36
|
+
"groups": { "type": "keyword" },
|
37
|
+
"perms": { "type": "keyword" },
|
38
|
+
"first": { "enabled": false },
|
39
|
+
"last": { "type": "date", "format": "epoch_second" },
|
40
|
+
"active": { "type": "boolean" }
|
41
|
+
}}}
|
42
|
+
}'.freeze,
|
43
|
+
}.freeze
|
44
|
+
|
45
|
+
public
|
46
|
+
|
47
|
+
###############################################
|
48
|
+
# New instance
|
49
|
+
#
|
50
|
+
# @param map [Hash] Symbol to String of the indexes.
|
51
|
+
# Must provide :user
|
52
|
+
# @param es [Faraday] Faraday instance to the Elasticsearch cluster
|
53
|
+
# @param src [Users] Source of authoritative information
|
54
|
+
# @param exp [Integer] Maximum time to cache a response
|
55
|
+
#
|
56
|
+
def initialize(map, es, src, exp=3600)
|
57
|
+
@map = map
|
58
|
+
@es = es
|
59
|
+
@src = src
|
60
|
+
@exp = exp
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
###############################################
|
65
|
+
# Validate a user
|
66
|
+
#
|
67
|
+
ValUserCache = {
|
68
|
+
method: :hash,
|
69
|
+
required: {
|
70
|
+
'name' => Items::FieldUsergrp,
|
71
|
+
'type' => {
|
72
|
+
method: :string,
|
73
|
+
allowed: Set[
|
74
|
+
'user'.freeze,
|
75
|
+
'role'.freeze,
|
76
|
+
'group'.freeze,
|
77
|
+
].freeze
|
78
|
+
}.freeze,
|
79
|
+
'first' => Validate::IsIntPos,
|
80
|
+
'last' => Validate::IsIntPos,
|
81
|
+
'active' => Validate::IsBoolean,
|
82
|
+
}.freeze,
|
83
|
+
optional: {
|
84
|
+
'roles' => {
|
85
|
+
method: :array,
|
86
|
+
check: Items::FieldUsergrp,
|
87
|
+
uniq: true
|
88
|
+
}.freeze,
|
89
|
+
'groups' => {
|
90
|
+
method: :array,
|
91
|
+
check: Items::FieldUsergrp,
|
92
|
+
uniq: true
|
93
|
+
}.freeze,
|
94
|
+
'perms' => {
|
95
|
+
method: :array,
|
96
|
+
check: Items::FieldPermGlobal,
|
97
|
+
uniq: true
|
98
|
+
}.freeze
|
99
|
+
}.freeze
|
100
|
+
}.freeze
|
101
|
+
|
102
|
+
|
103
|
+
###############################################
|
104
|
+
# (see Users#read)
|
105
|
+
#
|
106
|
+
def read(urg)
|
107
|
+
json = _read(:users, urg)
|
108
|
+
now = Time.now.to_i
|
109
|
+
|
110
|
+
# not in the cache
|
111
|
+
if !json
|
112
|
+
|
113
|
+
# read from source
|
114
|
+
obj = @src.read(urg)
|
115
|
+
return nil if !obj
|
116
|
+
|
117
|
+
# first time
|
118
|
+
obj['first'] = now
|
119
|
+
obj['last'] = now
|
120
|
+
obj['active'] = true
|
121
|
+
|
122
|
+
# store in cache
|
123
|
+
json = Validate.generate(obj, 'User/Role/Group'.freeze, ValUserCache)
|
124
|
+
_write(:users, urg, json)
|
125
|
+
|
126
|
+
# use cached version
|
127
|
+
else
|
128
|
+
obj = Validate.parse(json, 'User/Role/Group'.freeze, ValUserCache)
|
129
|
+
end
|
130
|
+
|
131
|
+
# expired
|
132
|
+
if (obj['last'] + @exp) < now
|
133
|
+
|
134
|
+
# read from source
|
135
|
+
obj2 = @src.read(urg)
|
136
|
+
|
137
|
+
# update
|
138
|
+
if obj2
|
139
|
+
obj['active'] = true
|
140
|
+
obj['roles'] = obj2['roles']
|
141
|
+
obj['groups'] = obj2['groups']
|
142
|
+
obj['perms'] = obj2['perms']
|
143
|
+
else
|
144
|
+
obj['active'] = false
|
145
|
+
end
|
146
|
+
obj['last'] = now
|
147
|
+
|
148
|
+
# and store in cache
|
149
|
+
json = Validate.generate(obj, 'User/Role/Group'.freeze, ValUserCache)
|
150
|
+
_write(:users, urg, json)
|
151
|
+
end
|
152
|
+
|
153
|
+
# not active
|
154
|
+
return nil unless obj['active']
|
155
|
+
|
156
|
+
# clean out cached info
|
157
|
+
obj.delete('first'.freeze)
|
158
|
+
obj.delete('last'.freeze)
|
159
|
+
obj.delete('active'.freeze)
|
160
|
+
|
161
|
+
return obj
|
162
|
+
end # def read()
|
163
|
+
|
164
|
+
end # class ICFS::Users
|
165
|
+
|
166
|
+
end # module ICFS
|
@@ -0,0 +1,132 @@
|
|
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 'tempfile'
|
13
|
+
require 'fileutils'
|
14
|
+
require 'set'
|
15
|
+
|
16
|
+
module ICFS
|
17
|
+
|
18
|
+
##########################################################################
|
19
|
+
# Implements {ICFS::Users Users} from a file system
|
20
|
+
#
|
21
|
+
class UsersFs < Users
|
22
|
+
|
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 = Validate.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
|
+
|
40
|
+
###############################################
|
41
|
+
# New instance
|
42
|
+
#
|
43
|
+
# @param path [String] Base directory
|
44
|
+
#
|
45
|
+
def initialize(path)
|
46
|
+
@path = path.dup
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
###############################################
|
51
|
+
# (see Users#read)
|
52
|
+
#
|
53
|
+
def read(urg)
|
54
|
+
Validate.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
|
96
|
+
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
|
+
|
107
|
+
rescue Errno::ENOENT
|
108
|
+
return nil
|
109
|
+
end # def read()
|
110
|
+
|
111
|
+
|
112
|
+
###############################################
|
113
|
+
# (see Users#write)
|
114
|
+
#
|
115
|
+
def write(obj)
|
116
|
+
Validate.validate(obj, 'User/Role/Group'.freeze, Users::ValUser)
|
117
|
+
json = JSON.pretty_generate(obj)
|
118
|
+
|
119
|
+
# write to temp file
|
120
|
+
tmp = Tempfile.new('_tmp'.freeze, @path, :encoding => 'ascii-8bit'.freeze)
|
121
|
+
tmp.write(json)
|
122
|
+
tmp.close
|
123
|
+
|
124
|
+
# move
|
125
|
+
FileUtils.mv(tmp.path, File.join(@path, obj['name'] + '.json'.freeze))
|
126
|
+
tmp.unlink
|
127
|
+
end # def write()
|
128
|
+
|
129
|
+
|
130
|
+
end # class ICFS::UsersFs
|
131
|
+
|
132
|
+
end # module ICFS
|
@@ -0,0 +1,479 @@
|
|
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 'set'
|
13
|
+
require 'json'
|
14
|
+
require 'tempfile'
|
15
|
+
|
16
|
+
module ICFS
|
17
|
+
|
18
|
+
##########################################################################
|
19
|
+
# Object validation
|
20
|
+
#
|
21
|
+
# @todo Move .parse and .generate to items or ICFS
|
22
|
+
# @todo Remove all use of Error module
|
23
|
+
#
|
24
|
+
module Validate
|
25
|
+
|
26
|
+
|
27
|
+
###############################################
|
28
|
+
# Parse JSON string and validate
|
29
|
+
#
|
30
|
+
# @param json [String] the JSON to parse
|
31
|
+
# @param name [String] description of the item
|
32
|
+
# @param val [Hash] the check to use
|
33
|
+
# @return [Object] the item
|
34
|
+
#
|
35
|
+
# @raise [Error::NotFound] if json is nil
|
36
|
+
# @raise [Error::Value] if parsing or validation fails
|
37
|
+
#
|
38
|
+
def self.parse(json, name, val)
|
39
|
+
if json.nil?
|
40
|
+
raise(Error::NotFound, '%s not found'.freeze % name)
|
41
|
+
end
|
42
|
+
begin
|
43
|
+
itm = JSON.parse(json)
|
44
|
+
rescue
|
45
|
+
raise(Error::Value, 'JSON parsing failed'.freeze)
|
46
|
+
end
|
47
|
+
Validate.validate(itm, name, val)
|
48
|
+
return itm
|
49
|
+
end # def self.parse()
|
50
|
+
|
51
|
+
|
52
|
+
###############################################
|
53
|
+
# Validate and generate JSON
|
54
|
+
#
|
55
|
+
# @param itm [Object] item to validate
|
56
|
+
# @param name [String] description of the item
|
57
|
+
# @param val [Hash] the check to use
|
58
|
+
# @return [String] JSON encoded item
|
59
|
+
#
|
60
|
+
# @raise [Error::Value] if validation fails
|
61
|
+
#
|
62
|
+
def self.generate(itm, name, val)
|
63
|
+
Validate.validate(itm, name, val)
|
64
|
+
return JSON.pretty_generate(itm)
|
65
|
+
end # def self.generate()
|
66
|
+
|
67
|
+
|
68
|
+
###############################################
|
69
|
+
# Validate an object
|
70
|
+
#
|
71
|
+
# @param obj [Object] object to validate
|
72
|
+
# @param name [String] description of the object
|
73
|
+
# @param val [Hash] the check to use
|
74
|
+
#
|
75
|
+
# @raise [Error::Value] if validation fails
|
76
|
+
#
|
77
|
+
def self.validate(obj, name, val)
|
78
|
+
err = Validate.check(obj, val)
|
79
|
+
if err
|
80
|
+
raise(Error::Value, '%s has bad values: %s'.freeze %
|
81
|
+
[name, err.inspect])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
###############################################
|
87
|
+
# check an object
|
88
|
+
#
|
89
|
+
# @param obj [Object] object to validate
|
90
|
+
# @param val [Hash] the check to use
|
91
|
+
# @return [Object] error description
|
92
|
+
#
|
93
|
+
def self.check(obj, val)
|
94
|
+
if val.key?(:object)
|
95
|
+
err = val[:object].send(val[:method], obj, val)
|
96
|
+
else
|
97
|
+
err = Validate.send(val[:method], obj, val)
|
98
|
+
end
|
99
|
+
return err
|
100
|
+
end # def self.check()
|
101
|
+
|
102
|
+
|
103
|
+
##############################################################
|
104
|
+
# Check Methods
|
105
|
+
##############################################################
|
106
|
+
|
107
|
+
|
108
|
+
###############################################
|
109
|
+
# check that any one validation is good
|
110
|
+
#
|
111
|
+
# @param obj [Object] object to validate
|
112
|
+
# @param val [Hash] options
|
113
|
+
# @option val [Array<Hash>] :check validations to check
|
114
|
+
# @return [Array,NilClass] error descriptions
|
115
|
+
#
|
116
|
+
def self.any(obj, val)
|
117
|
+
return nil unless val[:check].is_a?(Array)
|
118
|
+
|
119
|
+
errors = []
|
120
|
+
|
121
|
+
val[:check].each do |chk|
|
122
|
+
err = Validate.check(obj, chk)
|
123
|
+
return nil if err.nil?
|
124
|
+
errors << err
|
125
|
+
end
|
126
|
+
|
127
|
+
return errors
|
128
|
+
end # def self.any()
|
129
|
+
|
130
|
+
|
131
|
+
###############################################
|
132
|
+
# check that all the validations are good
|
133
|
+
#
|
134
|
+
# @param obj [Object] object to validate
|
135
|
+
# @param val [Hash] options
|
136
|
+
# @option val [Array<Hash>] :check validations to check
|
137
|
+
# @option val [Boolean] :all Always check all the validations
|
138
|
+
# @return [Array, NilClass] error descriptions
|
139
|
+
#
|
140
|
+
def self.all(obj, val)
|
141
|
+
return nil unless val[:check].is_a?(Array)
|
142
|
+
|
143
|
+
errors = []
|
144
|
+
bad = false
|
145
|
+
|
146
|
+
val[:check].each do |check|
|
147
|
+
err = Validate.check(obj, check)
|
148
|
+
if err
|
149
|
+
errors << err
|
150
|
+
bad = true
|
151
|
+
break unless val[:all]
|
152
|
+
else
|
153
|
+
errors << nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
return bad ? errors : nil
|
158
|
+
end # def self.all()
|
159
|
+
|
160
|
+
|
161
|
+
###############################################
|
162
|
+
# Check for an exact value
|
163
|
+
#
|
164
|
+
# @param obj [Object] object to validate
|
165
|
+
# @param val [Hash] options
|
166
|
+
# @option val [Integer] :check Value to compare
|
167
|
+
# @return [String,NilClass] error descriptions
|
168
|
+
#
|
169
|
+
def self.equals(obj, val)
|
170
|
+
if val[:check] == obj
|
171
|
+
return nil
|
172
|
+
else
|
173
|
+
return 'not equal'.freeze
|
174
|
+
end
|
175
|
+
end # def self.equals()
|
176
|
+
|
177
|
+
|
178
|
+
###############################################
|
179
|
+
# check an integer
|
180
|
+
#
|
181
|
+
# @param obj [Object] object to validate
|
182
|
+
# @param val [Hash] options
|
183
|
+
# @option val [Integer] :min Minimum value
|
184
|
+
# @option val [Integer] :max Maximum value
|
185
|
+
# @return [String,NilClass] error descriptions
|
186
|
+
#
|
187
|
+
#
|
188
|
+
def self.integer(obj, val)
|
189
|
+
return 'not an Integer'.freeze unless obj.is_a?(Integer)
|
190
|
+
|
191
|
+
if val[:min] && obj < val[:min]
|
192
|
+
return 'too small: %d < %d'.freeze % [obj, val[:min]]
|
193
|
+
end
|
194
|
+
|
195
|
+
if val[:max] && obj > val[:max]
|
196
|
+
return 'too large: %d > %d '.freeze % [obj, val[:max]]
|
197
|
+
end
|
198
|
+
|
199
|
+
return nil
|
200
|
+
end # def self.integer()
|
201
|
+
|
202
|
+
|
203
|
+
###############################################
|
204
|
+
# check a float
|
205
|
+
#
|
206
|
+
# @param obj [Object] object to validate
|
207
|
+
# @param val [Hash] options
|
208
|
+
# @option val [Float] :min Minimum value
|
209
|
+
# @option val [Float] :max Maximum value
|
210
|
+
# @return [String,NilClass] error descriptions
|
211
|
+
#
|
212
|
+
def self.float(obj, val)
|
213
|
+
return 'not a Float'.freeze unless obj.is_a?(Float)
|
214
|
+
|
215
|
+
if val[:min] && obj < val[:min]
|
216
|
+
return 'too small: %f < %f'.freeze % [obj, val[:min]]
|
217
|
+
end
|
218
|
+
|
219
|
+
if val[:max] && obj > val[:max]
|
220
|
+
return 'too large: %f > %f'.freeze % [obj, val[:max]]
|
221
|
+
end
|
222
|
+
|
223
|
+
return nil
|
224
|
+
end # def self.float()
|
225
|
+
|
226
|
+
|
227
|
+
###############################################
|
228
|
+
# check for a type
|
229
|
+
#
|
230
|
+
# @param obj [Object] object to validate
|
231
|
+
# @param val [Hash] options
|
232
|
+
# @option val [Class,Array] :type The class or module to check
|
233
|
+
# @return [String,NilClass] error descriptions
|
234
|
+
#
|
235
|
+
def self.type(obj, val)
|
236
|
+
if val[:type]
|
237
|
+
if val[:type].is_a?(Array)
|
238
|
+
val[:type].each{|cl| return nil if obj.is_a?(cl) }
|
239
|
+
return 'not a listed type'.freeze
|
240
|
+
else
|
241
|
+
if !obj.is_a?(val[:type])
|
242
|
+
return 'not a %s'.freeze % val[:type].name
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
return nil
|
247
|
+
end # def self.type
|
248
|
+
|
249
|
+
|
250
|
+
###############################################
|
251
|
+
# check a string
|
252
|
+
#
|
253
|
+
# @param obj [Object] object to validate
|
254
|
+
# @param val [Hash] options
|
255
|
+
# @option val [#include?] :allowed Value which is always okay
|
256
|
+
# @option val [#match] :valid check for okay value
|
257
|
+
# @option val [Boolean] :whitelist Must be valid or allowed
|
258
|
+
# @option val [#match] :invalid check for bad values
|
259
|
+
# @option val [Integer] :min Minimum length
|
260
|
+
# @option val [Integer] :max Maximum length
|
261
|
+
# @return [Hash,NilClass] error descriptions
|
262
|
+
#
|
263
|
+
def self.string(obj, val)
|
264
|
+
|
265
|
+
# type
|
266
|
+
return 'not a String'.freeze unless obj.is_a?(String)
|
267
|
+
|
268
|
+
errors = {}
|
269
|
+
|
270
|
+
# good values
|
271
|
+
if (val[:allowed] && val[:allowed].include?(obj)) ||
|
272
|
+
(val[:valid] && val[:valid].match(obj))
|
273
|
+
return nil
|
274
|
+
end
|
275
|
+
|
276
|
+
# if whitelisting
|
277
|
+
if val[:whitelist]
|
278
|
+
errors[:whitelist] = 'Value was not whitelisted'.freeze
|
279
|
+
end
|
280
|
+
|
281
|
+
# min length
|
282
|
+
if val[:min] && obj.size < val[:min]
|
283
|
+
errors[:min] = 'too short: %d < %d' % [obj.size, val[:min]]
|
284
|
+
end
|
285
|
+
|
286
|
+
# max length
|
287
|
+
if val[:max] && obj.size > val[:max]
|
288
|
+
errors[:max] = 'too long: %d > %d' % [obj.size, val[:max]]
|
289
|
+
end
|
290
|
+
|
291
|
+
# invalid
|
292
|
+
if val[:invalid] && val[:invalid].match(obj)
|
293
|
+
errors[:invalid] = true
|
294
|
+
end
|
295
|
+
|
296
|
+
return errors.empty? ? nil : errors
|
297
|
+
end # def self.string()
|
298
|
+
|
299
|
+
|
300
|
+
###############################################
|
301
|
+
# check an array
|
302
|
+
#
|
303
|
+
# @param obj [Object] object to validate
|
304
|
+
# @param val [Hash] options
|
305
|
+
# @option val [Integer] :min Minimum length
|
306
|
+
# @option val [Integer] :max Maximum length
|
307
|
+
# @option val [TrueClass] :uniq Require all members to be unique
|
308
|
+
# @option val [Hash,Array] :check Validations for members of the array.
|
309
|
+
# If a Hash is provided, all members will be checked against it.
|
310
|
+
# If an Array is provided, they will be checked in order.
|
311
|
+
# @return [Hash,NilClass] error descriptions
|
312
|
+
#
|
313
|
+
def self.array(obj, val)
|
314
|
+
|
315
|
+
# type
|
316
|
+
return 'not an Array'.freeze unless obj.is_a?(Array)
|
317
|
+
|
318
|
+
errors = {}
|
319
|
+
|
320
|
+
# min size
|
321
|
+
if val[:min] && obj.size < val[:min]
|
322
|
+
errors[:min] = true
|
323
|
+
end
|
324
|
+
|
325
|
+
# max size
|
326
|
+
if val[:max] && obj.size > val[:max]
|
327
|
+
errors[:max] = true
|
328
|
+
end
|
329
|
+
|
330
|
+
# all members uniq
|
331
|
+
if val[:uniq] && obj.size != obj.uniq.size
|
332
|
+
errors[:uniq] = true
|
333
|
+
end
|
334
|
+
|
335
|
+
# single check, all items of the array
|
336
|
+
if val[:check].is_a?(Hash)
|
337
|
+
check = val[:check]
|
338
|
+
|
339
|
+
# each value
|
340
|
+
obj.each_index do |ix|
|
341
|
+
if val[ix]
|
342
|
+
err = Validate.check(obj[ix], val[ix])
|
343
|
+
else
|
344
|
+
err = Validate.check(obj[ix], check)
|
345
|
+
end
|
346
|
+
errors[ix] = err if err
|
347
|
+
end
|
348
|
+
|
349
|
+
# an array of checks
|
350
|
+
elsif val[:check].is_a?(Array)
|
351
|
+
cka = val[:check]
|
352
|
+
cs = cka.size
|
353
|
+
|
354
|
+
# each value
|
355
|
+
obj.each_index do |ix|
|
356
|
+
if val[ix]
|
357
|
+
err = Validate.check(obj[ix], val[ix])
|
358
|
+
else
|
359
|
+
err = Validate.check(obj[ix], cka[ix % cs])
|
360
|
+
end
|
361
|
+
errors[ix] = err if err
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
return errors.empty? ? nil : errors
|
366
|
+
end # def self.array()
|
367
|
+
|
368
|
+
|
369
|
+
###############################################
|
370
|
+
# check a hash
|
371
|
+
#
|
372
|
+
# @param obj [Object] object to validate
|
373
|
+
# @param val [Hash] options
|
374
|
+
# @option val [Hash] :required Keys which must be present and their checks
|
375
|
+
# @option val [Hash] :optional Keys which may be present and their checks
|
376
|
+
# @option val [TrueClass] :others Allow other keys
|
377
|
+
# @return [Hash,NilClass] error descriptions
|
378
|
+
#
|
379
|
+
def self.hash(obj, val)
|
380
|
+
|
381
|
+
# type
|
382
|
+
return 'not a Hash'.freeze unless obj.is_a?(Hash)
|
383
|
+
|
384
|
+
ary = obj.to_a
|
385
|
+
chk = Array.new(ary.size)
|
386
|
+
errors = {}
|
387
|
+
|
388
|
+
# check all required keys
|
389
|
+
if val[:required]
|
390
|
+
val[:required].each do |key, check|
|
391
|
+
|
392
|
+
# find the index
|
393
|
+
ix = ary.index{|ok, ov| ok == key }
|
394
|
+
|
395
|
+
# missing required key
|
396
|
+
if ix.nil?
|
397
|
+
errors[key] = 'missing'.freeze
|
398
|
+
next
|
399
|
+
end
|
400
|
+
|
401
|
+
# check it
|
402
|
+
err = Validate.check(ary[ix][1], check)
|
403
|
+
errors[key] = err if err
|
404
|
+
chk[ix] = true
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# check all optional keys
|
409
|
+
if val[:optional]
|
410
|
+
val[:optional].each do |key, check|
|
411
|
+
|
412
|
+
# find the index
|
413
|
+
ix = ary.index{|ok, ov| ok == key }
|
414
|
+
next if ix.nil?
|
415
|
+
|
416
|
+
# do the check
|
417
|
+
err = Validate.check(ary[ix][1], check)
|
418
|
+
errors[key] = err if err
|
419
|
+
chk[ix] = true
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
# make sure we have validated all keys
|
424
|
+
if !val[:others]
|
425
|
+
chk.each_index do |ix|
|
426
|
+
next if chk[ix]
|
427
|
+
errors[ary[ix][0]] = 'not allowed'.freeze
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# do we have any errors?
|
432
|
+
return errors.empty? ? nil : errors
|
433
|
+
|
434
|
+
end # def self.hash()
|
435
|
+
|
436
|
+
|
437
|
+
##############################################################
|
438
|
+
# Common checks
|
439
|
+
##############################################################
|
440
|
+
|
441
|
+
|
442
|
+
# Boolean
|
443
|
+
IsBoolean = {
|
444
|
+
method: :type,
|
445
|
+
type: [ TrueClass, FalseClass ].freeze,
|
446
|
+
}.freeze
|
447
|
+
|
448
|
+
|
449
|
+
# Tempfile
|
450
|
+
IsTempfile = {
|
451
|
+
method: :type,
|
452
|
+
type: Tempfile,
|
453
|
+
}.freeze
|
454
|
+
|
455
|
+
|
456
|
+
# Float
|
457
|
+
IsFloat = {
|
458
|
+
method: :type,
|
459
|
+
type: [ Float ].freeze
|
460
|
+
}.freeze
|
461
|
+
|
462
|
+
|
463
|
+
# Positive Integer
|
464
|
+
IsIntPos = {
|
465
|
+
method: :integer,
|
466
|
+
min: 1
|
467
|
+
}.freeze
|
468
|
+
|
469
|
+
|
470
|
+
# Unsigned Integer
|
471
|
+
IsIntUns = {
|
472
|
+
method: :integer,
|
473
|
+
min: 0
|
474
|
+
}.freeze
|
475
|
+
|
476
|
+
|
477
|
+
end # module ICFS::Validate
|
478
|
+
|
479
|
+
end # module ICFS
|