cratus 0.2.1

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