jgrouper 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+