jgrouper 0.0.5 → 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.
@@ -0,0 +1,330 @@
1
+ require 'csv'
2
+ require 'date'
3
+
4
+ module JGrouper # :nodoc:
5
+ #
6
+ # = JGrouper::AuditArchiver - Archive Grouper audit log entries
7
+ #
8
+ # == USAGE
9
+ #
10
+ # require 'jgrouper'
11
+ # require 'jgrouper/audit_archiver'
12
+ #
13
+ # JGrouper::AuditArchiver.new { |archiver| archiver.archive }
14
+ #
15
+ class AuditArchiver
16
+
17
+ GROUPER_AUDIT_ENTRY = 'grouper_audit_entry'
18
+ GROUPER_AUDIT_TYPE = 'grouper_audit_type'
19
+
20
+ attr_writer :directory, :skip_columns, :verbose
21
+
22
+ def initialize
23
+ @config = {}
24
+ @conn = nil
25
+ @directory = Dir.pwd
26
+ @fh = nil
27
+ @mappings = {}
28
+ @skip_columns = []
29
+ @start, @stop = Time.at(0), Time.at(0)
30
+ @date = nil
31
+ @verbose = false
32
+ yield self if block_given?
33
+ self
34
+ end
35
+
36
+ #
37
+ # Archive oldest date from 'grouper_audit_entry' table to file.
38
+ #
39
+ def archive
40
+ entries = []
41
+
42
+ log 'starting ...'
43
+
44
+ connect do
45
+ mappings do
46
+ start_at = time_to_microseconds @start
47
+ stop_at = time_to_microseconds @stop
48
+
49
+ filehandle @directory, "grouper-audit-entries-#{ @date }.csv" do
50
+ log "archiving #{ @date } ..."
51
+
52
+ # TODO Extract!
53
+ # TODO Use prepared statement when I get JRuby, JDBC & Oracle to better cooperate on BigDecimal-ish data types
54
+ qry = "SELECT * FROM #{GROUPER_AUDIT_ENTRY} WHERE created_on BETWEEN #{start_at} AND #{stop_at} ORDER BY created_on"
55
+ stmt = @conn.create_statement
56
+ rs = stmt.execute_query qry
57
+ md = rs.meta_data
58
+ num_cols = md.column_count
59
+ while rs.next
60
+ entry = []
61
+ 1.upto(num_cols) do |n|
62
+ k = md.column_name(n).downcase.to_sym
63
+ v = rs.get_object(n)
64
+
65
+ next if v.nil?
66
+ entry << k << v.to_s.gsub(/\n/, ', ')
67
+ end
68
+
69
+ entry = transform(entry)
70
+ entries << entry.each_slice(2).reject { |slice| skip? slice.first }.collect { |slice| "#{slice.first}=#{slice.last}" }
71
+
72
+ @fh.puts CSV.generate_line( entries.last, col_sep: "\t" )
73
+ yield entries.last if block_given?
74
+ end
75
+ rs.close
76
+ stmt.close
77
+
78
+ log "archiving #{ @date } (#{entries.size} entries) - done"
79
+ end
80
+
81
+ prune(entries.size, start_at, stop_at) unless entries.empty?
82
+ end
83
+ end
84
+
85
+ log 'done'
86
+
87
+ entries
88
+ end
89
+
90
+
91
+ private
92
+
93
+ #
94
+ # Extract Grouper Hibernate configuration parameters
95
+ #
96
+ def configure
97
+ java_import edu.internet2.middleware.grouper.internal.dao.hibernate.HibernateDaoConfig
98
+ cfg = HibernateDaoConfig.new
99
+ %w( driver_class url username password autocommit ).each do |p|
100
+ k = "hibernate.connection.#{p}"
101
+ v = cfg.get_property k
102
+
103
+ if 'password' == p && File.exist?(v)
104
+ java_import edu.internet2.middleware.morphString.Morph
105
+ v = Morph.decryptIfFile(v)
106
+ end
107
+
108
+ @config[ p.to_sym ] = v
109
+ end
110
+ yield self
111
+ end
112
+
113
+ #
114
+ # Connect to Grouper database
115
+ #
116
+ def connect
117
+ configure do
118
+
119
+ java_import java.sql.DriverManager
120
+ java_import @config[:driver_class]
121
+
122
+ @conn = DriverManager.get_connection @config[:url], @config[:username], @config[:password]
123
+ @conn.auto_commit = false
124
+ @date = oldest_entry unless @date # Date of oldest entry
125
+ @start = @date.to_time # Start-of-day on @date
126
+ @stop = ( @date + 1 ).to_time - 1 # End-of-day on @date
127
+ yield self if block_given?
128
+ @conn.close
129
+
130
+ end
131
+ end
132
+
133
+ #
134
+ # Open filehandle for writing or raise exception if a) directory does not exist or b) file does exist.
135
+ # TODO DRY
136
+ #
137
+ def filehandle(directory, file)
138
+ raise "ERROR: directory does not exist - #{directory}" unless File.directory?(directory)
139
+ fn = File.join directory, file
140
+ raise "ERROR: file already exists - #{fn}" if File.exists?(fn)
141
+ File.open(fn, 'w') { |fh| @fh = fh ; yield self ; @fh = nil }
142
+ end
143
+
144
+ # TODO Use Logger...
145
+ # TODO DRY
146
+ def log(msg)
147
+ puts "#{ File.basename(__FILE__) } #{ Time.now.to_datetime.rfc3339 } - #{msg}" if verbose?
148
+ end
149
+
150
+ #
151
+ # Fetch audit type mappings.
152
+ #
153
+ def mappings
154
+ stmt = @conn.create_statement
155
+ rs = stmt.execute_query "SELECT * FROM #{GROUPER_AUDIT_TYPE}"
156
+ md = rs.meta_data
157
+ num_cols = md.column_count
158
+
159
+ while rs.next
160
+ tmp = {}
161
+ 1.upto(num_cols) do |n|
162
+ k = md.column_name(n).downcase.to_sym
163
+ case k
164
+ when :created_on, :last_updated
165
+ v = microseconds_to_rfc3399 rs.float(n)
166
+ else
167
+ v = rs.get_object(n)
168
+ end
169
+ next if v.nil?
170
+ tmp[k] = v.nil? ? v : v.to_s
171
+ end
172
+ @mappings[ tmp[:id] ] = tmp unless tmp.empty?
173
+ end
174
+
175
+ rs.close
176
+ stmt.close
177
+
178
+ yield self
179
+ end
180
+
181
+ #
182
+ # Convert microseconds to RFC3399 formatted timestamp.
183
+ #
184
+ def microseconds_to_rfc3399(microseconds)
185
+ Time.at( microseconds_to_seconds( microseconds.to_i ) ).to_datetime.rfc3339
186
+ end
187
+
188
+ #
189
+ # Convert microseconds to seconds.
190
+ #
191
+ def microseconds_to_seconds(microseconds)
192
+ microseconds / 1000
193
+ end
194
+
195
+ #
196
+ # Return number of entries for time interval.
197
+ #
198
+ def num_entries(start_at, stop_at)
199
+ num_entries = 0
200
+
201
+ stmt = @conn.create_statement
202
+ rs = stmt.execute_query "SELECT COUNT(*) FROM #{GROUPER_AUDIT_ENTRY} WHERE created_on BETWEEN #{start_at} AND #{stop_at}"
203
+ while rs.next
204
+ num_entries = rs.get_object(1).to_s.to_i # TODO Ugly
205
+ break
206
+ end
207
+ rs.close
208
+ stmt.close
209
+
210
+ return num_entries
211
+ end
212
+
213
+ #
214
+ # Calculate archive date based on MIN(created_on)
215
+ #
216
+ def oldest_entry
217
+ d = Date.new
218
+
219
+ stmt = @conn.create_statement
220
+ rs = stmt.execute_query "SELECT MIN(created_on) FROM #{GROUPER_AUDIT_ENTRY}"
221
+ while rs.next
222
+ # Microseconds -> Seconds -> Time -> Date
223
+ d = Time.at( microseconds_to_seconds( rs.float(1) ) ).to_date
224
+ break
225
+ end
226
+ rs.close
227
+ stmt.close
228
+
229
+ d
230
+ end
231
+
232
+ #
233
+ # Prune entries from database.
234
+ #
235
+ def prune(expected, start_at, stop_at)
236
+ log "pruning #{ @date } (#{expected} entries) ..."
237
+
238
+ found = num_entries start_at, stop_at
239
+ if expected == found
240
+
241
+ @conn.auto_commit = false
242
+ qry = "DELETE FROM #{GROUPER_AUDIT_ENTRY} WHERE created_on BETWEEN #{start_at} AND #{stop_at}"
243
+ stmt = @conn.create_statement
244
+ rv = stmt.execute_update qry
245
+ if expected == rv
246
+ @conn.commit
247
+ log "pruning #{ @date } (#{rv} entries) - done"
248
+ end
249
+ stmt.close
250
+ unless expected == rv
251
+ raise "ERROR: not committing delete as number of deleted entries does not match expected number - #{rv} != #{expected}"
252
+ end
253
+ else
254
+ raise "ERROR: not deleting as number of found entries does not match expected number - #{found} != #{expected}"
255
+ end
256
+ end
257
+
258
+ #
259
+ # Omit column from export?
260
+ #
261
+ def skip?(name)
262
+ @skip_columns.include? name
263
+ end
264
+
265
+ #
266
+ # Convert time to microseconds
267
+ #
268
+ def time_to_microseconds(time)
269
+ time.to_i * 1000
270
+ end
271
+
272
+ #
273
+ # Perhaps basic data transformations to make audit entry more readable.
274
+ #
275
+ def transform(entry)
276
+ # TODO Better validation & error handling
277
+ idx = entry.index(:audit_type_id)
278
+
279
+ if idx
280
+ type = @mappings[ entry[ idx + 1 ] ]
281
+ if type && type[:audit_category] && type[:action_name]
282
+ # TODO Or just replace :audit_type_id entirely?
283
+ entry.insert idx + 2, :audit_category
284
+ entry.insert idx + 3, type[:audit_category]
285
+ entry.insert idx + 4, :action_name
286
+ entry.insert idx + 5, type[:action_name]
287
+ end
288
+ end
289
+
290
+ entry.each_with_index do |v, idx|
291
+ if idx.even?
292
+ case v
293
+ when :created_on, :last_updated
294
+ entry[ idx + 1 ] = microseconds_to_rfc3399 entry[ idx + 1 ]
295
+ when :logged_in_member_id
296
+ entry[ idx + 1 ] = transform_member_uuid entry[ idx + 1 ]
297
+ else
298
+ label_key = "label_#{v}".to_sym
299
+ if type.include? label_key
300
+ entry[idx] = "#{v}:#{ type[label_key] }"
301
+ case type[label_key]
302
+ when 'memberId'
303
+ entry[ idx + 1 ] = transform_member_uuid entry[ idx + 1 ]
304
+ end
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end
310
+
311
+ #
312
+ # Transform Grouper member UUID into something more useful.
313
+ #
314
+ def transform_member_uuid(uuid)
315
+ m = JGrouper::Member.find uuid
316
+ return uuid if m.nil?
317
+ return "#{uuid} (source=#{ m.subject_source_id } id=#{ m.subject_id } type=#{ m.subject_type })"
318
+ end
319
+
320
+ #
321
+ # Are we verbose?
322
+ #
323
+ def verbose?
324
+ @verbose
325
+ end
326
+
327
+ end
328
+ end
329
+
330
+
@@ -0,0 +1,85 @@
1
+ require 'csv'
2
+
3
+ module JGrouper # :nodoc:
4
+ #
5
+ # = JGrouper::Exporter - Export (some of the) Groups registry to CSV
6
+ #
7
+ # == USAGE
8
+ #
9
+ # require 'jgrouper'
10
+ # require 'jgrouper/exporter'
11
+ #
12
+ # JGrouper::Exporter do |exporter|
13
+ # exporter.export 'stem:to:export'
14
+ # end
15
+ #
16
+ class Exporter
17
+
18
+ attr_writer :verbose
19
+
20
+ def initialize
21
+ @directory = Dir.pwd
22
+ @fh = nil
23
+ @verbose = false
24
+ yield self if block_given?
25
+ self
26
+ end
27
+
28
+ #
29
+ # Export (some of the) Groups registry to CSV.
30
+ #
31
+ def export( base = nil )
32
+ entries = []
33
+
34
+ filehandle @directory, "jgrouper-export.csv" do
35
+ stem = base.nil? ? Stem.root : Stem.find(base)
36
+ raise "ERROR: stem not found" if stem.nil?
37
+ log "exporting stem=#{stem.name} ..."
38
+
39
+ raise "NOT IMPLEMENTED - exporting child stems" if stem.stems.size > 0
40
+
41
+ groups = stem.groups do |child|
42
+ g = %w( display_extension extension name ).collect { |k| child.send(k) }
43
+ g << CSV.generate_line( child.members.collect { |m| "#{ m.subject_source_id }|#{ m.subject_type_id }|#{ m.subject_id }" } ).chomp
44
+
45
+ entries << CSV.generate_line(g)
46
+ @fh.puts entries.last
47
+ yield entries.last if block_given?
48
+ end
49
+
50
+ log "exporting stem=#{stem.name} - done"
51
+ end
52
+
53
+ entries
54
+ end
55
+
56
+
57
+ private
58
+
59
+ #
60
+ # Open filehandle for writing or raise exception if a) directory does not exist or b) file does exist.
61
+ # TODO DRY
62
+ #
63
+ def filehandle(directory, file)
64
+ raise "ERROR: directory does not exist - #{directory}" unless File.directory?(directory)
65
+ fn = File.join directory, file
66
+ raise "ERROR: file already exists - #{fn}" if File.exists?(fn)
67
+ File.open(fn, 'w') { |fh| @fh = fh ; yield self ; @fh = nil }
68
+ end
69
+
70
+ # TODO Use Logger...
71
+ # TODO DRY
72
+ def log(msg)
73
+ puts "#{ File.basename(__FILE__) } #{ Time.now.to_datetime.rfc3339 } - #{msg}" if verbose?
74
+ end
75
+
76
+ #
77
+ # Are we verbose?
78
+ #
79
+ def verbose?
80
+ @verbose
81
+ end
82
+
83
+ end
84
+ end
85
+
@@ -1,116 +1,40 @@
1
+ require 'csv'
2
+
1
3
  module JGrouper # :nodoc:
4
+
5
+ #
6
+ # = JGrouper::Group - Grouper Group
7
+ #
8
+ # == Usage
9
+ #
10
+ # require 'jgrouper'
2
11
  #
3
- # = JGrouper::Group - A Grouper group
12
+ # TODO
4
13
  #
5
14
  class Group
6
15
 
7
- attr_reader :display_extension, :display_name, :extension, :name, :types, :uuid
8
-
9
-
10
- def initialize
11
- @grouper_group = nil
16
+ def initialize( obj = nil )
17
+ @obj = obj
12
18
  yield self if block_given?
13
19
  self
14
20
  end
15
21
 
16
22
  #
17
- # Add group type. Returns +true+ if type added or group already has type.
18
- #
19
- # group.add_type 'group type'
20
- #
21
- def add_type(name)
22
- return true if types.include? name
23
- t = Java::EduInternet2MiddlewareGrouper::GroupTypeFinder.find name, false
24
- return false unless t
25
- @grouper_group.add_type t
26
- true
27
- end
28
-
29
- #
30
- # Delete group type. Returns +true+ if type deleted or group does not have type.
31
- #
32
- # group.delete_type 'group type'
33
- #
34
- def delete_type(name)
35
- t = Java::EduInternet2MiddlewareGrouper::GroupTypeFinder.find name, false
36
- return false unless t
37
- return true unless types.include? name
38
- @grouper_group.delete_type t
39
- true
40
- end
41
-
42
- #
43
- # Find group by name or returns +nil+.
23
+ # For passing methods on to Grouper Group object.
44
24
  #
45
- # group = JGrouper::Group.find(name)
46
- #
47
- def self.find(name)
48
- group = from_grouper GroupFinder.findByName( GrouperSession.startRootSession, name, false ) # How to handle sessions?
49
- yield group if block_given?
50
- group
51
- end
52
-
53
- #
54
- # Generate JGrouper::Group instance from JSON by calling +JSON.parse(json_string)+
55
- #
56
- def self.json_create(json)
57
- new do |group|
58
- group.instance_variable_set :@display_extension, json['display_extension']
59
- group.instance_variable_set :@display_name, json['display_name']
60
- group.instance_variable_set :@extension, json['extension']
61
- group.instance_variable_set :@name, json['name']
62
- group.instance_variable_set :@types, json['types']
63
- group.instance_variable_set :@uuid, json['uuid']
64
- end
65
- end
66
-
67
25
  def method_missing(meth, *args, &block)
26
+ super if @obj.nil?
68
27
  begin
69
- warn "METHOD_MISSING: meth=#{ meth } args=#{ args }"
70
- block.call @grouper_group.send(meth, *args) if block
71
- @grouper_group.send(meth, *args)
28
+ block.call @obj.send(meth, *args) if block
29
+ @obj.send(meth, *args)
72
30
  rescue NoMethodError
73
31
  super
74
32
  end
75
33
  end
76
34
 
77
- #
78
- # Return JSON representation of JGrouper::Group instance.
79
- #
80
- def to_json
81
- {
82
- :json_class => self.class.name,
83
- :display_extension => self.display_extension,
84
- :display_name => self.display_name,
85
- :extension => self.extension,
86
- :name => self.name,
87
- :types => self.types,
88
- :uuid => self.uuid
89
- }.to_json
90
- end
91
-
92
- #
93
- # Return String representation of JGrouper::Group instance.
94
- #
95
35
  def to_s
96
- %w( name display_name uuid ).collect { |k| "#{k}=#{ self.send(k) }" }.join(' | ') + " | types=#{ types.join(',') }"
97
- end
98
-
99
-
100
- private
101
-
102
- def self.from_grouper(grouper)
103
- return nil if grouper.nil?
104
- new do |group|
105
- group.instance_variable_set :@grouper_group, grouper
106
- group.instance_variable_set :@display_extension, grouper.getDisplayExtension
107
- group.instance_variable_set :@display_name, grouper.getDisplayName
108
- group.instance_variable_set :@extension, grouper.getExtension
109
- group.instance_variable_set :@name, grouper.getName
110
- # TODO Testing hack
111
- group.instance_variable_set :@types, grouper.getTypes.collect { |t| t.kind_of?(String) ? t : t.getName }
112
- group.instance_variable_set :@uuid, grouper.getUuid
113
- end
36
+ return nil if @obj.nil?
37
+ CSV.generate_line %w( name display_name uuid ).collect { |k| "#{k}=#{ self.send(k) }" }
114
38
  end
115
39
 
116
40
  end
@@ -0,0 +1,47 @@
1
+ module JGrouper # :nodoc:
2
+
3
+ #
4
+ # = JGrouper::Member - Grouper Member
5
+ #
6
+ # == Usage
7
+ #
8
+ # require 'jgrouper'
9
+ #
10
+ # m = JGrouper::Member.find uuid
11
+ #
12
+ class Member
13
+
14
+ # TODO
15
+ def initialize( obj = nil )
16
+ @obj = obj
17
+ yield self if block_given?
18
+ self
19
+ end
20
+
21
+ #
22
+ # Find Grouper member by UUID. Returns +JGrouper::Member+ or +nil+.
23
+ #
24
+ def self.find(uuid)
25
+ m = MemberFinder.find_by_uuid GrouperSession.start_root_session, uuid, false
26
+ return nil if m.nil?
27
+ member = self.new m
28
+ yield member if block_given?
29
+ member
30
+ end
31
+
32
+ #
33
+ # For passing methods on to Grouper member object.
34
+ #
35
+ def method_missing(meth, *args, &block)
36
+ super if @obj.nil?
37
+ begin
38
+ block.call @obj.send(meth, *args) if block
39
+ @obj.send(meth, *args)
40
+ rescue NoMethodError
41
+ super
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+