cratus 0.2.1

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: edca85708a85f952e904d928dbaa959fd9788289
4
+ data.tar.gz: fed75393016468b8451f926fee9836dde5b6b519
5
+ SHA512:
6
+ metadata.gz: 3640dd6ba0f250a6c02be3f8d07b092e0a7235e27b27a62839325b4db6eef546506d6607f7d22065f56b8527ac15d6703269f7b206cdecf6795bf378bc9d0d82
7
+ data.tar.gz: e4e42674b01a639246e984d13170671550705f75cd2b2af961b788729aeaa0d12e82e8a2aab705c844db8388df7158cb8aba2e068680ae794f15b98088473095
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 KnuEdge Corporation
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'cratus'
4
+
5
+ include Cratus
6
+
7
+ LDAP.connect
8
+
9
+ # Read in arguments
10
+ group_mapping_arg = ARGV[0]
11
+ raise "Missing Group Mapping Argument!" unless group_mapping_arg
12
+ group_mapping_file = File.expand_path(group_mapping_arg)
13
+ raise "Invalid Group Mapping File #{group_mapping_file}" unless File.readable?(group_mapping_file)
14
+
15
+ # Load the YAML file(s) for mapping permissions
16
+ group_permissions = YAML.load_file(group_mapping_file)
17
+
18
+ # Gather group memberships and permissions
19
+ @results = {} # stash all the results here... might get really big
20
+ User.all.sort.each do |user|
21
+ key = user.username.to_s
22
+ user_groups = user.member_of.map { |g| g.name.to_s }
23
+
24
+ @results[key] = {'groups' => {}}
25
+ user_groups.sort.each do |ugroup|
26
+ group_perm_set = group_permissions[ugroup] ? group_permissions[ugroup].sort : []
27
+ @results[key]['groups'][ugroup] = group_perm_set
28
+ end
29
+ end
30
+
31
+ puts @results.to_yaml
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+ require 'tempfile'
5
+
6
+ # Used to compare the output of the `cratus` command across multiple runs
7
+
8
+ input1 = ARGV[0]
9
+ input2 = ARGV[1]
10
+
11
+ # Find our diff command and break if we don't have one
12
+ diff_path = `which diff`.chomp
13
+ raise "Missing diff command in PATH!" unless $?.success?
14
+
15
+ # The command we'll use later to see what has changed
16
+ diff_cmd = "#{diff_path} -U 999999"
17
+
18
+ ## Methods / Functions
19
+ def validate_input(input)
20
+ # Make sure the input is set
21
+ raise "Missing First Input!" unless input
22
+ # Make sure the input is a valid file that we can read
23
+ raise "Invalid Input File #{input}" unless File.readable?(input)
24
+ # TODO: make sure the input file is valid YAML
25
+ end
26
+
27
+ # Read in some input
28
+ def read_input(input)
29
+ begin
30
+ YAML.load_file(input)
31
+ rescue => e
32
+ raise "Unable to open #{input}: #{e.message}"
33
+ end
34
+ end
35
+
36
+ ## Execution
37
+ validate_input(input1)
38
+ validate_input(input2)
39
+
40
+ data1 = read_input(input1)
41
+ data2 = read_input(input2)
42
+
43
+ if data1 == data2
44
+ exit 0
45
+ else
46
+ STDERR.puts "Looks like things have changed!"
47
+
48
+ # Calculate what has actually changed
49
+ additions_and_differences = (data2.to_a - data1.to_a)
50
+ removals = (data1.to_a - data2.to_a)
51
+ results = {}
52
+ additions_and_differences.each do |user|
53
+ results[user[0]] = user[1]
54
+ end
55
+
56
+ # Temporary files for a call to `diff`
57
+ newdata = Tempfile.new('new')
58
+ olddata = Tempfile.new('old')
59
+
60
+ # Add the results from our comparison to the first temp file
61
+ newdata.write(results.to_yaml)
62
+
63
+ # Grab the old data for just our changed users and put it into a new hash
64
+ oldhash = {}
65
+ results.each do |user,data|
66
+ oldhash[user] = data1[user] if data1.key?(user)
67
+ end
68
+ # Add things that were removed from the old data (removed users)
69
+ removals.each do |removed_user|
70
+ oldhash[removed_user[0]] = removed_user[1]
71
+ end
72
+
73
+ # Write out the old data
74
+ olddata.write(oldhash.to_yaml)
75
+
76
+ # Close and flush our temp files
77
+ newdata.close
78
+ olddata.close
79
+
80
+ # The actual call to the `diff` command
81
+ system("#{diff_cmd} #{olddata.path} #{newdata.path}")
82
+
83
+ # Destroy the temp files after the diff command finishes
84
+ newdata.unlink
85
+ olddata.unlink
86
+ end
87
+
@@ -0,0 +1,14 @@
1
+ # Standard Library
2
+ require 'ostruct'
3
+ require 'yaml'
4
+
5
+ # External Requirements
6
+ require 'net/ldap'
7
+
8
+ # Internal Requirements
9
+ require 'cratus/version'
10
+ require 'cratus/config'
11
+ Cratus.config.load
12
+ require 'cratus/ldap'
13
+ require 'cratus/group'
14
+ require 'cratus/user'
@@ -0,0 +1,65 @@
1
+ # A generic way of constructing a mergeable configuration
2
+ class Cratus::Config < OpenStruct
3
+ # Construct a base config using the following order of precedence:
4
+ # * environment variables
5
+ # * YAML file
6
+ # * defaults
7
+ def load
8
+ # First, apply the defaults
9
+ defaults = {
10
+ group_dn_attribute: :cn,
11
+ group_member_attribute: :member,
12
+ group_description_attribute: :description,
13
+ group_objectclass: :group,
14
+ group_basedn: 'ou=groups,dc=example,dc=com',
15
+ group_memberof_attribute: :memberOf,
16
+ user_dn_attribute: :samaccountname,
17
+ user_objectclass: :user,
18
+ user_basedn: 'ou=users,dc=example,dc=com',
19
+ user_department_attribute: :department,
20
+ user_lockout_attribute: :lockouttime,
21
+ user_mail_attribute: :mail,
22
+ user_displayname_attribute: :displayName,
23
+ user_memberof_attribute: :memberOf,
24
+ host: 'ldap.example.com',
25
+ port: 389,
26
+ basedn: 'dc=example,dc=com',
27
+ username: 'username',
28
+ password: 'p@assedWard!',
29
+ }
30
+ merge defaults
31
+
32
+ # Then apply the config file, if one exists
33
+ begin
34
+ apprc_dir = File.expand_path('~')
35
+ config_file = File.expand_path(File.join(apprc_dir, '.cratus.yml'))
36
+ merge YAML.load_file(config_file) if File.readable?(config_file)
37
+ rescue => e
38
+ puts "WARNING: Unable to read from #{config_file}"
39
+ end
40
+
41
+ # Finally, apply any environment variables specified
42
+ env_conf = {}
43
+ defaults.keys.each do |key|
44
+ cratus_key = "CRATUS_#{key}".upcase
45
+ env_conf[key] = ENV[cratus_key] if ENV.key?(cratus_key)
46
+ end
47
+ merge env_conf unless env_conf.empty?
48
+ end
49
+
50
+ def merge(data)
51
+ raise 'Invalid Config Data' unless data.is_a?(Hash)
52
+ data.each do |k, v|
53
+ self[k.to_sym] = v
54
+ end
55
+ end
56
+ end
57
+
58
+ # Make the config available as a singleton
59
+ module Cratus
60
+ class << self
61
+ def config
62
+ @config ||= Config.new
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,131 @@
1
+ module Cratus
2
+ class Group
3
+ include Comparable
4
+ attr_reader :name, :search_base
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ @search_base = self.class.ldap_search_base
9
+ @raw_ldap_data = Cratus::LDAP.search(
10
+ "(#{self.class.ldap_dn_attribute}=#{@name})",
11
+ basedn: @search_base,
12
+ attrs: self.class.ldap_return_attributes
13
+ ).last
14
+ end
15
+
16
+ # LDAP users that are a member of this group
17
+ def members
18
+ all_members[:users]
19
+ end
20
+
21
+ def member_groups
22
+ all_members[:groups]
23
+ end
24
+
25
+ def member_of
26
+ memrof_attr = Cratus.config.group_memberof_attribute
27
+
28
+ # TODO make this work with more things...
29
+ unless @raw_ldap_data
30
+ STDERR.puts "WARNING: Group '#{@name}' appears to be invalid or beyond the search scope!"
31
+ return []
32
+ end
33
+
34
+ # TODO: move the search filter to a configurable param
35
+ raw_groups = @raw_ldap_data[memrof_attr].reject {|g| g.match /OU=Distribution Groups/ }
36
+ initial_groups = raw_groups.map do |raw_group|
37
+ Group.new(raw_group.match(/^#{Group.ldap_dn_attribute.to_s.upcase}=([^,]+),/)[1])
38
+ end
39
+ all_the_groups = initial_groups
40
+ initial_groups.each do |group|
41
+ all_the_groups.concat(group.member_of) # recursion!
42
+ end
43
+ all_the_groups.uniq { |g| g.name }
44
+ end
45
+
46
+ # LDAP description attribute
47
+ def description
48
+ @raw_ldap_data[Cratus.config.group_description_attribute].last
49
+ end
50
+
51
+ # All the LDAP Groups
52
+ def self.all
53
+ filter = "(#{ldap_dn_attribute}=*)"
54
+ Cratus::LDAP.search(filter, basedn: ldap_search_base, attrs: ldap_dn_attribute).map do |entry|
55
+ self.new(entry[ldap_dn_attribute].last)
56
+ end
57
+ end
58
+
59
+ def self.ldap_dn_attribute
60
+ Cratus.config.group_dn_attribute.to_s
61
+ end
62
+
63
+ def self.ldap_object_class
64
+ Cratus.config.group_objectclass.to_s
65
+ end
66
+
67
+ def self.ldap_return_attributes
68
+ [
69
+ Cratus.config.group_dn_attribute.to_s,
70
+ Cratus.config.group_member_attribute.to_s,
71
+ Cratus.config.group_description_attribute.to_s,
72
+ Cratus.config.group_memberof_attribute.to_s
73
+ ]
74
+ end
75
+
76
+ def self.ldap_search_base
77
+ Cratus.config.group_basedn.to_s
78
+ end
79
+
80
+ def <=>(other)
81
+ @name <=> other.name
82
+ end
83
+
84
+ private
85
+
86
+ # provides a Hash of member users and groups
87
+ def all_members
88
+ # filters used to determine if each group member is a User or Group
89
+ group_filter = "(objectClass=#{Cratus.config.group_objectclass.to_s})"
90
+ user_filter = "(objectClass=#{Cratus.config.user_objectclass.to_s})"
91
+
92
+ # The raw LDAP data (a list of DNs)
93
+ raw_members = @raw_ldap_data[Cratus.config.group_member_attribute]
94
+
95
+ # Somewhere to store users and groups as we gather them
96
+ results = { users: [], groups: [] }
97
+
98
+ # Iterate over the members and provide a user or group
99
+ raw_members.each do |member|
100
+ user_result = Cratus::LDAP.search(
101
+ user_filter,
102
+ basedn: member,
103
+ scope: 'object',
104
+ attrs: User.ldap_return_attributes
105
+ )
106
+
107
+ if !user_result.nil? && !user_result.empty?
108
+ results[:users] << User.new(user_result.last[User.ldap_dn_attribute].last)
109
+ else
110
+ group_result = Cratus::LDAP.search(
111
+ group_filter,
112
+ basedn: member,
113
+ scope: 'object',
114
+ attrs: self.class.ldap_return_attributes
115
+ )
116
+ unless group_result.nil? || group_result.empty?
117
+ nested_group = Group.new(group_result.last[self.class.ldap_dn_attribute].last)
118
+ results[:groups] << nested_group
119
+ results[:groups].concat(nested_group.member_groups)
120
+ results[:users].concat(nested_group.members)
121
+ end
122
+ end
123
+ end
124
+
125
+ # deliver the results
126
+ results[:groups].uniq! { |g| g.name }
127
+ results[:users].uniq! { |u| u.username }
128
+ return results
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,88 @@
1
+ module Cratus
2
+ module LDAP
3
+ # Define the LDAP connection
4
+ # Note: does not actually connect (bind), just sets up the connection
5
+ def self.connection
6
+ options = {
7
+ host: Cratus.config.host,
8
+ port: Cratus.config.port,
9
+ base: Cratus.config.basedn,
10
+ auth: {
11
+ method: :simple,
12
+ username: Cratus.config.username,
13
+ password: Cratus.config.password
14
+ }
15
+ }
16
+ # TODO: make the validations do something useful
17
+ #validate_connection_options(options)
18
+ @@ldap_connection ||= Net::LDAP.new(options)
19
+ end
20
+
21
+ # Actually connect (bind) to LDAP
22
+ def self.connect
23
+ connection
24
+ validate_ldap_connection
25
+ @@ldap_connection.bind
26
+ @@ldap_bound = true
27
+ end
28
+
29
+ # Perform an LDAP search
30
+ #
31
+ # Required Options: :basedn
32
+ # Optional Options: :attrs, :scope
33
+ def self.search(filter, options = {})
34
+ validate_ldap_connection
35
+ validate_ldap_bound
36
+ validate_search_options(options)
37
+
38
+ attrs = options.key?(:attrs) ? options[:attrs] : []
39
+ scope = options.key?(:scope) ? options[:scope] : 'subtree'
40
+
41
+ scope_class = case scope.to_s
42
+ when 'subtree','recursive','whole_subtree'
43
+ Net::LDAP::SearchScope_WholeSubtree
44
+ when 'single','single_level'
45
+ Net::LDAP::SearchScope_SingleLevel
46
+ when 'object','base_object'
47
+ Net::LDAP::SearchScope_BaseObject
48
+ else
49
+ fail "Invalid LDAP Scope!"
50
+ end
51
+
52
+ results = @@ldap_connection.search(
53
+ base: options[:basedn],
54
+ filter: filter,
55
+ scope: scope_class,
56
+ attributes: [*attrs].map(&:to_s)
57
+ )
58
+ raise "Search Failed" if results.nil?
59
+ results.compact
60
+ end
61
+
62
+ # Validation Methods
63
+
64
+ def self.validate_ldap_bound
65
+ raise "LDAP Not Connected" unless defined? @@ldap_bound
66
+ end
67
+
68
+ def self.validate_ldap_connection
69
+ raise "No LDAP Connection" unless defined? @@ldap_connection
70
+ end
71
+
72
+ def self.validate_search_options(options)
73
+ raise "Invalid Options" unless options.respond_to?(:key?)
74
+
75
+ [:basedn].each do |key|
76
+ raise "Missing Option: #{key}" unless options.key?(key)
77
+ end
78
+ end
79
+
80
+ def self.validate_connection_options(options)
81
+ raise "Invalid Options" unless options.respond_to?(:key?)
82
+
83
+ [:host, :port, :basedn, :username, :password].each do |key|
84
+ raise "Missing Option: #{key}" unless options.key?(key)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,91 @@
1
+ module Cratus
2
+ class User
3
+ include Comparable
4
+ attr_reader :username, :search_base
5
+
6
+ def initialize(username)
7
+ @username = username
8
+ @search_base = self.class.ldap_search_base
9
+ @raw_ldap_data = Cratus::LDAP.search(
10
+ "(#{self.class.ldap_dn_attribute}=#{@username})",
11
+ basedn: @search_base,
12
+ attrs: self.class.ldap_return_attributes
13
+ ).last
14
+ end
15
+
16
+ def department
17
+ @raw_ldap_data[Cratus.config.user_department_attribute].last
18
+ end
19
+
20
+ def email
21
+ @raw_ldap_data[Cratus.config.user_mail_attribute].last
22
+ end
23
+
24
+ def fullname
25
+ @raw_ldap_data[Cratus.config.user_displayname_attribute].last
26
+ end
27
+
28
+ def lockouttime
29
+ @raw_ldap_data[Cratus.config.user_lockout_attribute].last
30
+ end
31
+
32
+ def locked?
33
+ lockouttime != '0'
34
+ end
35
+
36
+ def member_of
37
+ memrof_attr = Cratus.config.user_memberof_attribute
38
+ # TODO: move the search filter to a configurable param
39
+ raw_groups = @raw_ldap_data[memrof_attr].reject {|g| g.match /OU=Distribution Groups/ }
40
+ initial_groups = raw_groups.map do |raw_group|
41
+ Group.new(raw_group.match(/^#{Group.ldap_dn_attribute.to_s.upcase}=([^,]+),/)[1])
42
+ end
43
+ all_the_groups = initial_groups
44
+ initial_groups.each do |group|
45
+ all_the_groups.concat(group.member_of)
46
+ end
47
+ all_the_groups.uniq { |g| g.name }
48
+ end
49
+
50
+ alias_method :groups, :member_of
51
+
52
+ def <=>(other)
53
+ @username <=> other.username
54
+ end
55
+
56
+ # All the LDAP Users
57
+ def self.all
58
+ raw_results = Cratus::LDAP.search(
59
+ "(objectClass=#{ldap_object_class})",
60
+ basedn: ldap_search_base,
61
+ attrs: ldap_dn_attribute
62
+ )
63
+ raw_results.map do |entry|
64
+ self.new(entry[ldap_dn_attribute.to_sym].last)
65
+ end
66
+ end
67
+
68
+ def self.ldap_dn_attribute
69
+ Cratus.config.user_dn_attribute.to_s
70
+ end
71
+
72
+ def self.ldap_object_class
73
+ Cratus.config.user_objectclass.to_s
74
+ end
75
+
76
+ def self.ldap_return_attributes
77
+ [
78
+ Cratus.config.user_dn_attribute.to_s,
79
+ Cratus.config.user_department_attribute.to_s,
80
+ Cratus.config.user_mail_attribute.to_s,
81
+ Cratus.config.user_displayname_attribute.to_s,
82
+ Cratus.config.user_memberof_attribute.to_s,
83
+ Cratus.config.user_lockout_attribute.to_s
84
+ ]
85
+ end
86
+
87
+ def self.ldap_search_base
88
+ Cratus.config.user_basedn.to_s
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,8 @@
1
+ module Cratus
2
+ def self.version
3
+ major = 0 # Breaking, incompatible releases
4
+ minor = 2 # Compatible, but new features
5
+ patch = 1 # Fixes to existing features
6
+ [major, minor, patch].map(&:to_s).join('.')
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cratus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Gnagy
8
+ - Daniel Schaaff
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-11-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: colorize
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.7'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '0.7'
28
+ - !ruby/object:Gem::Dependency
29
+ name: net-ldap
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '0.10'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.10'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.1'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.1'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rubocop
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '0.35'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '0.35'
70
+ - !ruby/object:Gem::Dependency
71
+ name: yard
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.8'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.8'
84
+ description: The Ruby tool for auditing and reporting on user permissions based on
85
+ groups
86
+ email: jgnagy@knuedge.com
87
+ executables:
88
+ - cratus
89
+ - cratus-compare
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - LICENSE
94
+ - bin/cratus
95
+ - bin/cratus-compare
96
+ - lib/cratus.rb
97
+ - lib/cratus/config.rb
98
+ - lib/cratus/group.rb
99
+ - lib/cratus/ldap.rb
100
+ - lib/cratus/user.rb
101
+ - lib/cratus/version.rb
102
+ homepage:
103
+ licenses:
104
+ - MIT
105
+ metadata: {}
106
+ post_install_message: Thanks for installing Cratus!
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '2.2'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 2.5.1
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Cratus queries LDAP for users and their memberships, then reports on it.
126
+ test_files: []