macadmin 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +163 -0
- data/Rakefile +23 -0
- data/ext/macadmin/password/crypto.c +89 -0
- data/ext/macadmin/password/extconf.rb +3 -0
- data/lib/macadmin.rb +22 -0
- data/lib/macadmin/common.rb +80 -0
- data/lib/macadmin/dslocal.rb +252 -0
- data/lib/macadmin/dslocal/computer.rb +22 -0
- data/lib/macadmin/dslocal/computergroup.rb +19 -0
- data/lib/macadmin/dslocal/dslocalnode.rb +281 -0
- data/lib/macadmin/dslocal/group.rb +82 -0
- data/lib/macadmin/dslocal/user.rb +113 -0
- data/lib/macadmin/mcx.rb +227 -0
- data/lib/macadmin/password.rb +72 -0
- data/lib/macadmin/shadowhash.rb +297 -0
- data/lib/macadmin/version.rb +8 -0
- data/macadmin.gemspec +35 -0
- data/spec/common_spec.rb +49 -0
- data/spec/computer_spec.rb +133 -0
- data/spec/computergroup_spec.rb +274 -0
- data/spec/dslocal_spec.rb +179 -0
- data/spec/dslocalnode_spec.rb +343 -0
- data/spec/group_spec.rb +275 -0
- data/spec/macadmin_spec.rb +13 -0
- data/spec/mcx_spec.rb +145 -0
- data/spec/password_spec.rb +40 -0
- data/spec/shadowhash_spec.rb +309 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/user_spec.rb +248 -0
- metadata +218 -0
@@ -0,0 +1,252 @@
|
|
1
|
+
module MacAdmin
|
2
|
+
|
3
|
+
# Stub Error class
|
4
|
+
class DSLocalError < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
# DSLocalRecord (super class)
|
8
|
+
# - this is the raw constructor class for DSLocal records
|
9
|
+
# - records of 'type' should be created using one of the provided subclasses
|
10
|
+
# - this class delegates to Hash and therefore behaves as though it were one
|
11
|
+
# - added method_missing? to do fancy dot-style attribute returns
|
12
|
+
class DSLocalRecord < DelegateClass(Hash)
|
13
|
+
|
14
|
+
include MacAdmin::Common
|
15
|
+
include MacAdmin::MCX
|
16
|
+
|
17
|
+
# Where all the files on disk live
|
18
|
+
DSLOCAL_ROOT = '/private/var/db/dslocal/nodes'
|
19
|
+
|
20
|
+
# Some reader attributes for introspection and debugging
|
21
|
+
attr_reader :data, :composite, :real, :file, :record, :node
|
22
|
+
attr_accessor :file
|
23
|
+
|
24
|
+
class << self
|
25
|
+
|
26
|
+
# Inits a record from a file on disk
|
27
|
+
# - param is a path to a DSLocal Property List file
|
28
|
+
# - if file is invalid, return nil
|
29
|
+
def init_with_file(file)
|
30
|
+
data = load_plist file
|
31
|
+
return nil unless data
|
32
|
+
self.new :name => data['name'].first, :file => file, :real => data
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create a new DSLocalRecord
|
38
|
+
# - this method is not meant to be called directly; use subclasses instead
|
39
|
+
# - params are valid DSLocalRecord attributes
|
40
|
+
# - when a node is not specified, 'Default' is assumed
|
41
|
+
def initialize(args)
|
42
|
+
@real = (args.delete(:real) { nil }) unless args.is_a? String
|
43
|
+
@data = normalize(args)
|
44
|
+
@name = @data['name'].first
|
45
|
+
@record_type = record_type
|
46
|
+
@node = (@data.delete('node') { ['Default'] }).first.to_s
|
47
|
+
@file = (@data.delete('file') { ["#{DSLOCAL_ROOT}/#{@node}/#{@record_type + 's'}/#{@name}.plist"] }).first.to_s
|
48
|
+
@record = synthesize(@data)
|
49
|
+
super(@record)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Does the specified resource already exist?
|
53
|
+
# - returns Boolean
|
54
|
+
def exists?
|
55
|
+
@real = load_plist @file
|
56
|
+
@composite.eql? @real
|
57
|
+
end
|
58
|
+
|
59
|
+
# Create the record
|
60
|
+
# - simply writes the compiled Hash to disk
|
61
|
+
# - converts ShadowHashData attrib to CFPropertyList::Blob before writing
|
62
|
+
# - will accept an alternate path than the default; useful for debugging
|
63
|
+
def create(file = @file)
|
64
|
+
out = @record.dup
|
65
|
+
if shadowhashdata = out['ShadowHashData']
|
66
|
+
out['ShadowHashData'] = [CFPropertyList::Blob.new(shadowhashdata.first)]
|
67
|
+
end
|
68
|
+
plist = CFPropertyList::List.new
|
69
|
+
plist.value = CFPropertyList.guess(out)
|
70
|
+
plist.save(file, CFPropertyList::List::FORMAT_BINARY)
|
71
|
+
FileUtils.chmod(0600, file)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Delete the record
|
75
|
+
# - removes the file representing the record from disk
|
76
|
+
# - will accept an alternate path than the default; useful for debugging
|
77
|
+
# - returns true if the file was destroyed or does not exist; false otherwise
|
78
|
+
def destroy(file = @file)
|
79
|
+
FileUtils.rm file if File.exists? file
|
80
|
+
!File.exists? file
|
81
|
+
end
|
82
|
+
|
83
|
+
# Test object equality
|
84
|
+
# - Class#eql? is not being passed to the delegate
|
85
|
+
# - it needs a little help
|
86
|
+
def eql?(obj)
|
87
|
+
if obj.is_a?(self.class)
|
88
|
+
return self.record.eql?(obj.record)
|
89
|
+
end
|
90
|
+
false
|
91
|
+
end
|
92
|
+
alias equal? eql?
|
93
|
+
|
94
|
+
# Diff two records
|
95
|
+
# - of limited value except for debugging
|
96
|
+
# - output is not very coherent
|
97
|
+
def diff(other)
|
98
|
+
this = self.record
|
99
|
+
other = other.record
|
100
|
+
(this.keys + other.keys).uniq.inject({}) do |memo, key|
|
101
|
+
unless this[key] == other[key]
|
102
|
+
if this[key].kind_of?(Hash) && other[key].kind_of?(Hash)
|
103
|
+
memo[key] = this[key].diff(other[key])
|
104
|
+
else
|
105
|
+
memo[key] = [this[key], other[key]]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
memo
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Override the Hash getter method
|
113
|
+
# - so that we can use Symbols as well as Strings
|
114
|
+
def [](key)
|
115
|
+
key = key.to_s if key.is_a?(Symbol)
|
116
|
+
super(key)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Override the Hash setter method
|
120
|
+
# - so that we can use Symbols as well as Strings
|
121
|
+
def []=(key, value)
|
122
|
+
key = key.to_s if key.is_a?(Symbol)
|
123
|
+
super(key, value)
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Synthesize a record
|
129
|
+
# - returns a composite record (Hash) compiled by merging the input data with a pre-existing matched record
|
130
|
+
# - if there is no matching record, missing attributes will be synthesized from defaults
|
131
|
+
# - returns an Hash stored in an instance variable: @composite
|
132
|
+
def synthesize(data)
|
133
|
+
@real ||= load_plist(@file)
|
134
|
+
if @real
|
135
|
+
@composite = @real.dup
|
136
|
+
@composite.merge!(data)
|
137
|
+
else
|
138
|
+
@composite = defaults(data)
|
139
|
+
end
|
140
|
+
@composite
|
141
|
+
end
|
142
|
+
|
143
|
+
# Handle required but unspecified record attributes
|
144
|
+
# - GUID is the only attribute common to all record types
|
145
|
+
def defaults(data)
|
146
|
+
next_guid = UUID.new
|
147
|
+
defaults = { 'generateduid' => ["#{next_guid}"] }
|
148
|
+
defaults.merge(data)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Format the user input so it can be processed
|
152
|
+
# - input is key/value pairs
|
153
|
+
# - keys are preserved
|
154
|
+
# - values are converted to arrays to satisfy record schema
|
155
|
+
def normalize(input)
|
156
|
+
name_error = "Name attribute only supports lowercase letters, hypens, and underscrores."
|
157
|
+
# If there's only a single arg, and it's a String, make it the :name attrib
|
158
|
+
input = input.is_a?(String) ? { 'name' => input } : input
|
159
|
+
result = input.inject({}){ |memo,(k,v)| memo[k.to_s] = [v.to_s]; memo }
|
160
|
+
raise DSLocalError.new(name_error) unless result['name'].first.match /^[_a-z0-9][a-z0-9_-]*$/
|
161
|
+
result
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns the type of record being instantiated
|
165
|
+
# - derived from class of object
|
166
|
+
# - returns String
|
167
|
+
def record_type
|
168
|
+
string = self.class.to_s
|
169
|
+
parts = string.split(/::/)
|
170
|
+
parts.last.to_s.downcase
|
171
|
+
end
|
172
|
+
|
173
|
+
# Get all records of type
|
174
|
+
# - returns Array of all records for the matching type
|
175
|
+
def all_records(node)
|
176
|
+
records = []
|
177
|
+
search_base = "#{DSLOCAL_ROOT}/#{node}/#{@record_type + 's'}"
|
178
|
+
files = Dir["#{search_base}/*.plist"]
|
179
|
+
unless files.empty?
|
180
|
+
files.each do |path|
|
181
|
+
records << eval("#{self.class}.init_with_file('#{path}')")
|
182
|
+
end
|
183
|
+
end
|
184
|
+
records
|
185
|
+
end
|
186
|
+
|
187
|
+
# For a set of records, get all attributes of type
|
188
|
+
# - params are: type (Symbol) and records (Array)
|
189
|
+
# - symbol is one of the valid DSLocalRecord attribute types (ie. :uid, :gid) as Symbol
|
190
|
+
# - array is a collection of records (see DSLocalRecord#all_records)
|
191
|
+
# - parses array of records and collects attribs of type
|
192
|
+
# - returns Array
|
193
|
+
def get_all_attribs_of_type(type, records)
|
194
|
+
type = type.to_s
|
195
|
+
begin
|
196
|
+
attribs = []
|
197
|
+
unless records.empty?
|
198
|
+
records.each do |record|
|
199
|
+
attrib = record[type]
|
200
|
+
next if attrib.empty?
|
201
|
+
attribs << attrib
|
202
|
+
end
|
203
|
+
end
|
204
|
+
rescue => error
|
205
|
+
puts "Ruby Error: #{error.message}"
|
206
|
+
end
|
207
|
+
attribs
|
208
|
+
end
|
209
|
+
|
210
|
+
# Given an array of id number attributes, find the next available id number
|
211
|
+
# - params are: min (Integer) and ids (Array)
|
212
|
+
# - scans the array and delivers the next free id number
|
213
|
+
# - returns String
|
214
|
+
def next_id(min, ids)
|
215
|
+
ids.flatten!
|
216
|
+
ids.collect! { |id| id.to_i }
|
217
|
+
begin
|
218
|
+
ids.sort!.uniq!
|
219
|
+
ids.each_with_index do |id, i|
|
220
|
+
next if (id < min)
|
221
|
+
next if (id + 1 == ids[i + 1])
|
222
|
+
return (id + 1).to_s
|
223
|
+
end
|
224
|
+
rescue => error
|
225
|
+
puts "Ruby Error: #{error.message}"
|
226
|
+
end
|
227
|
+
min.to_s
|
228
|
+
end
|
229
|
+
|
230
|
+
# Provide dot notation for setting and getting valid attribs
|
231
|
+
def method_missing(meth, *args, &block)
|
232
|
+
if args.empty?
|
233
|
+
return self[meth.to_s] if self[meth.to_s]
|
234
|
+
return nil if defaults(@data)[meth.to_s]
|
235
|
+
else
|
236
|
+
if meth.to_s =~ /=$/
|
237
|
+
if self["#{$`}"] or defaults(@data)["#{$`}"]
|
238
|
+
if args.is_a? Array
|
239
|
+
return self["#{$`}"] = (args.each { |e| e.to_s }).flatten
|
240
|
+
elsif args.is_a? String
|
241
|
+
return self["#{$`}"] = e.to_s
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
super
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module MacAdmin
|
2
|
+
|
3
|
+
# Computer
|
4
|
+
# - creates and manages Mac OS X Computer records
|
5
|
+
# - params: :name, :realname, :en_address
|
6
|
+
class Computer < DSLocalRecord
|
7
|
+
|
8
|
+
# Handle required but unspecified record attributes
|
9
|
+
# - generates missing attributes
|
10
|
+
# - changes are merged into the composite record
|
11
|
+
def defaults(data)
|
12
|
+
mac_address = get_primary_mac_address
|
13
|
+
defaults = {
|
14
|
+
'realname' => ["#{data['name'].first.capitalize}"],
|
15
|
+
'en_address' => [mac_address]
|
16
|
+
}
|
17
|
+
super defaults.merge(data)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module MacAdmin
|
2
|
+
|
3
|
+
# ComputerGroup
|
4
|
+
# - creates and manages AMC OS X Computer Groups
|
5
|
+
# - inherits from MacAdmin::Group
|
6
|
+
# - params: :name, :realname, :gid
|
7
|
+
class ComputerGroup < Group
|
8
|
+
|
9
|
+
MIN_GID = 501
|
10
|
+
|
11
|
+
def initialize(args)
|
12
|
+
@member_class = Computer unless defined? @member_class
|
13
|
+
@group_class = ComputerGroup unless defined? @group_class
|
14
|
+
super args
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
module MacAdmin
|
2
|
+
|
3
|
+
class DSLocalNodeError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
# DSLocalNode
|
7
|
+
# - constructs and manages Local OpenDirectory nodes
|
8
|
+
# - takes one parameter: name
|
9
|
+
# - if no name param is passed, 'Default' node is used
|
10
|
+
class DSLocalNode
|
11
|
+
|
12
|
+
require 'find'
|
13
|
+
|
14
|
+
SANDBOX_FILE = '/System/Library/Sandbox/Profiles/com.apple.opendirectoryd.sb'
|
15
|
+
PREFERENCES = '/Library/Preferences/OpenDirectory/Configurations/Search.plist'
|
16
|
+
PREFERENCES_LEGACY = '/Library/Preferences/DirectoryService/SearchNodeConfig.plist'
|
17
|
+
CHILD_DIRS = ['aliases', 'computer_lists', 'computergroups', 'computers', 'config', 'groups', 'networks', 'users']
|
18
|
+
DSLOCAL_ROOT = '/private/var/db/dslocal/nodes'
|
19
|
+
DIRMODE = 16832
|
20
|
+
FILEMODE = 33152
|
21
|
+
OWNER = 0
|
22
|
+
GROUP = 0
|
23
|
+
|
24
|
+
attr_reader :name, :label, :root
|
25
|
+
|
26
|
+
def initialize(name='Default')
|
27
|
+
@name = name
|
28
|
+
@label = "/Local/#{name}"
|
29
|
+
@root = "#{DSLOCAL_ROOT}/#{name}"
|
30
|
+
load_configuration_file
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Compound method: create and then activate the node
|
35
|
+
def create_and_activate
|
36
|
+
create
|
37
|
+
activate
|
38
|
+
end
|
39
|
+
|
40
|
+
# Compound method: destroy and then deactivate the node
|
41
|
+
def destroy_and_deactivate
|
42
|
+
destroy
|
43
|
+
deactivate
|
44
|
+
end
|
45
|
+
|
46
|
+
# Compound method: does the node exist and is it active?
|
47
|
+
def exists_and_active?
|
48
|
+
exists? and active?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Does the directory structure exist?
|
52
|
+
def exists?
|
53
|
+
validate_directory_structure
|
54
|
+
end
|
55
|
+
|
56
|
+
# Test whether or not the node is in the search path
|
57
|
+
# - also: test the sandbox configuration if required
|
58
|
+
def active?
|
59
|
+
if needs_sandbox?
|
60
|
+
return false unless sandbox_active?
|
61
|
+
end
|
62
|
+
load_configuration_file
|
63
|
+
if self.name.eql? 'Default'
|
64
|
+
case policy = self.searchpolicy
|
65
|
+
when Integer
|
66
|
+
return true if policy < 3
|
67
|
+
else
|
68
|
+
return true if policy =~ /\AdsAttrTypeStandard:[LN]SPSearchPath\z/
|
69
|
+
end
|
70
|
+
end
|
71
|
+
return false if cspsearchpath.nil?
|
72
|
+
return false unless searchpolicy_is_custom?
|
73
|
+
cspsearchpath.member?(@label)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Create the directory structure
|
77
|
+
def create
|
78
|
+
create_directories
|
79
|
+
end
|
80
|
+
|
81
|
+
# Destroy the directory structure
|
82
|
+
def destroy
|
83
|
+
FileUtils.rm_rf @root
|
84
|
+
end
|
85
|
+
|
86
|
+
# Add the node to the list of searchable directory services
|
87
|
+
# - also: add a sandbox configuration if required
|
88
|
+
def activate
|
89
|
+
activate_sandbox if needs_sandbox?
|
90
|
+
insert_node
|
91
|
+
set_custom_searchpolicy
|
92
|
+
save_config
|
93
|
+
end
|
94
|
+
|
95
|
+
# Remove the node to the list of searchable directory services
|
96
|
+
# - also: remove a sandbox configuration if required
|
97
|
+
def deactivate
|
98
|
+
deactivate_sandbox if needs_sandbox?
|
99
|
+
remove_node
|
100
|
+
save_config
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns the search policy
|
104
|
+
def searchpolicy
|
105
|
+
eval @policy_key
|
106
|
+
end
|
107
|
+
|
108
|
+
# Replaces the search policy
|
109
|
+
def searchpolicy=(val)
|
110
|
+
if val.is_a?(String)
|
111
|
+
eval @policy_key+"= val"
|
112
|
+
else
|
113
|
+
eval @policy_key+"= #{val}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns the search path array
|
118
|
+
def cspsearchpath
|
119
|
+
eval @paths_key
|
120
|
+
end
|
121
|
+
|
122
|
+
# Replaces the search path array
|
123
|
+
def cspsearchpath=(array)
|
124
|
+
eval @paths_key+"= array"
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Does this platform require a sandbox configuration?
|
130
|
+
def needs_sandbox?
|
131
|
+
MAC_OS_X_PRODUCT_VERSION > 10.7
|
132
|
+
end
|
133
|
+
|
134
|
+
# Produces a Regex for matching the OpenDirectory sandbox's "allow file-write" rules
|
135
|
+
def sb_regex(name = 'Default')
|
136
|
+
exemplar = %Q{#"^(/private)?/var/db/dslocal/nodes/Default(/|$)"}
|
137
|
+
pattern = name.eql?('Default') ? name : exemplar.sub(/Default/, name)
|
138
|
+
pattern = Regexp.escape pattern
|
139
|
+
Regexp.new pattern.gsub /\//,'\\/'
|
140
|
+
end
|
141
|
+
|
142
|
+
# Is the there an active sandbox for the node?
|
143
|
+
def sandbox_active?
|
144
|
+
if File.exists? SANDBOX_FILE
|
145
|
+
@sandbox = File.readlines(SANDBOX_FILE)
|
146
|
+
@sandbox.each { |line| return true if line.match sb_regex(@name) }
|
147
|
+
end
|
148
|
+
false
|
149
|
+
end
|
150
|
+
|
151
|
+
# Activate the node's sandbox
|
152
|
+
def activate_sandbox
|
153
|
+
unless sandbox_active?
|
154
|
+
@sandbox.each_with_index do |line, index|
|
155
|
+
if line.match sb_regex
|
156
|
+
@sandbox.insert index + 1, line.sub(/Default/, @name)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
File.open(SANDBOX_FILE, 'w') { |f| f << @sandbox }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# De-activate the node's sandbox
|
164
|
+
def deactivate_sandbox
|
165
|
+
if sandbox_active?
|
166
|
+
@sandbox.delete_if do |line|
|
167
|
+
line.match sb_regex @name
|
168
|
+
end
|
169
|
+
File.open(SANDBOX_FILE, 'w') { |f| f << @sandbox }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Insert the node into the search path immediately after any builtin local nodes
|
174
|
+
def insert_node
|
175
|
+
self.cspsearchpath ||= []
|
176
|
+
dslocal_node = '/Local/Default'
|
177
|
+
bsd_node = '/BSD/local'
|
178
|
+
|
179
|
+
unless self.cspsearchpath.include? @label
|
180
|
+
if index = cspsearchpath.index(bsd_node)
|
181
|
+
cspsearchpath.insert(index + 1, @label)
|
182
|
+
elsif index = cspsearchpath.index(dslocal_node)
|
183
|
+
cspsearchpath.insert(index + 1, @label)
|
184
|
+
else
|
185
|
+
cspsearchpath.unshift(@label)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
self.cspsearchpath.uniq!
|
189
|
+
end
|
190
|
+
|
191
|
+
# Remove the node from the search path
|
192
|
+
def remove_node
|
193
|
+
cspsearchpath.delete(@label)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Has custom ds searching been enabled?
|
197
|
+
def searchpolicy_is_custom?
|
198
|
+
searchpolicy.eql?(@custom)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Set the search opolicy to custom
|
202
|
+
def set_custom_searchpolicy
|
203
|
+
self.searchpolicy = @custom
|
204
|
+
end
|
205
|
+
|
206
|
+
# Save the configuraton file to disk
|
207
|
+
def save_config
|
208
|
+
plist = CFPropertyList::List.new
|
209
|
+
plist.value = CFPropertyList.guess(@config)
|
210
|
+
plist.save(@file, CFPropertyList::List::FORMAT_XML)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Check hierarchy and permissions and ownership are valid
|
214
|
+
# - returns bool
|
215
|
+
def validate_directory_structure
|
216
|
+
return false unless File.exists? @root
|
217
|
+
Find.find(@root) do |path|
|
218
|
+
stat = File::Stat.new path
|
219
|
+
return false unless stat.uid == OWNER and stat.gid == GROUP
|
220
|
+
if File.directory? path
|
221
|
+
return false unless stat.mode == DIRMODE
|
222
|
+
else
|
223
|
+
return false unless stat.mode == FILEMODE
|
224
|
+
end
|
225
|
+
end
|
226
|
+
true
|
227
|
+
end
|
228
|
+
|
229
|
+
# Create the dir structure for a DSLocal node
|
230
|
+
def create_directories
|
231
|
+
begin
|
232
|
+
FileUtils.mkdir_p @root unless File.exist? @root
|
233
|
+
FileUtils.chmod(0700, @root)
|
234
|
+
CHILD_DIRS.each do |child|
|
235
|
+
FileUtils.mkdir_p("#{@root}/#{child}") unless File.exist?("#{@root}/#{child}")
|
236
|
+
FileUtils.chmod(0700, "#{@root}/#{child}")
|
237
|
+
end
|
238
|
+
FileUtils.chown_R(OWNER, GROUP, @root)
|
239
|
+
rescue Exception => e
|
240
|
+
p e.message
|
241
|
+
p e.backtrace.inspect
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Decide which configuration file we should be trying to access
|
246
|
+
def get_configuration_file
|
247
|
+
file = PREFERENCES_LEGACY
|
248
|
+
file = PREFERENCES if File.exists? '/usr/libexec/opendirectoryd'
|
249
|
+
file
|
250
|
+
end
|
251
|
+
|
252
|
+
# If the file we need is still not on disk, we HUP the dir service
|
253
|
+
# Try 3 times, and then fail
|
254
|
+
def load_configuration_file
|
255
|
+
3.times do
|
256
|
+
@file = get_configuration_file
|
257
|
+
if File.exists? @file
|
258
|
+
break
|
259
|
+
else
|
260
|
+
restart_directoryservice(11)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
raise DSLocalNodeError "Cannot read the Search policy file, #{@file}" unless File.exists?(@file)
|
264
|
+
@config = load_plist @file
|
265
|
+
# Setup some configuration key paths that can be evaluated and plugged into
|
266
|
+
# the standard methods. Which paths are used is based on which config file
|
267
|
+
# we are working with.
|
268
|
+
if @config['modules']
|
269
|
+
@paths_key = %q{@config['modules']['session'][0]['options']['dsAttrTypeStandard:CSPSearchPath']}
|
270
|
+
@policy_key = %q{@config['modules']['session'][0]['options']['dsAttrTypeStandard:SearchPolicy']}
|
271
|
+
@custom = 'dsAttrTypeStandard:CSPSearchPath'
|
272
|
+
else
|
273
|
+
@paths_key = %q{@config['Search Node Custom Path Array']}
|
274
|
+
@policy_key = %q{@config['Search Policy']}
|
275
|
+
@custom = 3
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|