macadmin 0.0.4
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.
- 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
|