open_directory_utils 0.1.2 → 0.1.3

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.
@@ -1,4 +1,4 @@
1
- require "open_directory_utils/dscl"
1
+ # require "open_directory_utils/dscl"
2
2
  require "open_directory_utils/clean_check"
3
3
  require "open_directory_utils/commands_base"
4
4
 
@@ -8,7 +8,7 @@ module OpenDirectoryUtils
8
8
  # @note - these commands were derived from the following resrouces:
9
9
  # * https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/dscl.1.html
10
10
  # * https://superuser.com/questions/592921/mac-osx-users-vs-dscl-command-to-list-user/621055?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa
11
- module CommandsUserAttribsOd
11
+ module CommandsUserCreateRemove
12
12
 
13
13
  # include OpenDirectoryUtils::Dscl
14
14
  include OpenDirectoryUtils::CleanCheck
@@ -93,6 +93,13 @@ module OpenDirectoryUtils
93
93
  attribs[:value] = attribs[:value] || attribs[:surname]
94
94
  attribs[:value] = attribs[:value] || attribs[:lastname]
95
95
  attribs[:value] = attribs[:value] || attribs[:last_name]
96
+ attribs[:value] = attribs[:value] || attribs[:real_name]
97
+ attribs[:value] = attribs[:value] || attribs[:realname]
98
+ attribs[:value] = attribs[:value] || attribs[:short_name]
99
+ attribs[:value] = attribs[:value] || attribs[:shortname]
100
+ attribs[:value] = attribs[:value] || attribs[:user_name]
101
+ attribs[:value] = attribs[:value] || attribs[:username]
102
+ attribs[:value] = attribs[:value] || attribs[:uid]
96
103
 
97
104
  check_critical_attribute( attribs, :record_name )
98
105
  check_critical_attribute( attribs, :value, :last_name )
@@ -104,24 +111,18 @@ module OpenDirectoryUtils
104
111
  dscl( user_attrs, dir_info )
105
112
  end
106
113
 
107
- # /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -create /Users/$shortname_USERNAME NameSuffix "$VALUE"
108
- def user_set_name_suffix
109
- end
110
- # /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -create /Users/$shortname_USERNAME apple-namesuffix "$VALUE"
111
- def user_set_apple_name_suffix
112
- end
113
-
114
114
  # sudo dscl . -create /Users/someuser UniqueID "1010"
115
115
  def user_set_unique_id(attribs, dir_info)
116
116
  attribs = user_record_name_alternatives(attribs)
117
+ check_critical_attribute( attribs, :record_name )
117
118
 
118
119
  attribs[:value] = attribs[:value] || attribs[:uniqueid]
119
120
  attribs[:value] = attribs[:value] || attribs[:unique_id]
121
+ attribs[:value] = attribs[:value] || attribs[:uid_number]
120
122
  attribs[:value] = attribs[:value] || attribs[:uidnumber]
121
123
  attribs[:value] = attribs[:value] || attribs[:usernumber]
122
124
  attribs[:value] = attribs[:value] || attribs[:user_number]
123
125
 
124
- check_critical_attribute( attribs, :record_name )
125
126
  check_critical_attribute( attribs, :value, :unique_id )
126
127
  attribs = tidy_attribs(attribs)
127
128
 
@@ -184,11 +185,11 @@ module OpenDirectoryUtils
184
185
 
185
186
  answer = []
186
187
 
187
- command = {action: 'create', scope: 'Users', attribute: 'mail'}
188
+ command = {action: 'create', scope: 'Users', attribute: 'MailAttribute'}
188
189
  user_attrs = attribs.merge(command)
189
190
  answer << dscl( user_attrs, dir_info )
190
191
 
191
- command = {action: 'create', scope: 'Users', attribute: 'email'}
192
+ command = {action: 'create', scope: 'Users', attribute: 'EMailAttribute'}
192
193
  user_attrs = attribs.merge(command)
193
194
  answer << dscl( user_attrs, dir_info )
194
195
 
@@ -205,6 +206,8 @@ module OpenDirectoryUtils
205
206
 
206
207
  attribs[:value] = attribs[:value] || attribs['apple-user-mailattribute']
207
208
  attribs[:value] = attribs[:value] || attribs[:apple_user_mailattribute]
209
+ attribs[:value] = attribs[:value] || attribs[:e_mail_attribute]
210
+ attribs[:value] = attribs[:value] || attribs[:mail_attribute]
208
211
  attribs[:value] = attribs[:value] || attribs[:email]
209
212
  attribs[:value] = attribs[:value] || attribs[:mail]
210
213
 
@@ -323,9 +326,24 @@ module OpenDirectoryUtils
323
326
 
324
327
  dseditgroup( user_attrs, dir_info )
325
328
  end
326
- # module_function :user_add_to_group
327
- # alias_method :user_set_group_memebership, :user_add_to_group
328
329
 
330
+ def user_remove_from_group(attribs, dir_info)
331
+ attribs = user_record_name_alternatives(attribs)
332
+
333
+ attribs[:value] = attribs[:group_membership]
334
+ attribs[:value] = attribs[:value] || attribs[:groupmembership]
335
+ attribs[:value] = attribs[:value] || attribs[:group_name]
336
+ attribs[:value] = attribs[:value] || attribs[:groupname]
337
+ attribs[:value] = attribs[:value] || attribs[:gid]
338
+
339
+ check_critical_attribute( attribs, :record_name, :username )
340
+ check_critical_attribute( attribs, :value, :groupname )
341
+ attribs = tidy_attribs(attribs)
342
+ command = { operation: 'edit', action: 'delete', type: 'user'}
343
+ user_attrs = attribs.merge(command)
344
+
345
+ dseditgroup( user_attrs, dir_info )
346
+ end
329
347
 
330
348
  # /usr/bin/pwpolicy -a diradmin -p A-B1g-S3cret -u $shortname_USERNAME -getpolicy
331
349
  def user_get_policy(attribs, dir_info)
@@ -360,20 +378,29 @@ module OpenDirectoryUtils
360
378
  attribs[:value] = nil
361
379
  answer << user_set_password(attribs, dir_info)
362
380
  attribs[:value] = nil
381
+ answer << user_set_shell(attribs, dir_info)
382
+ attribs[:value] = nil
383
+ answer << user_set_last_name(attribs, dir_info)
384
+ attribs[:value] = nil
385
+ answer << user_set_real_name(attribs, dir_info)
386
+ attribs[:value] = nil
387
+ answer << user_set_unique_id(attribs, dir_info)
388
+ attribs[:value] = nil
389
+ answer << user_set_primary_group_id(attribs, dir_info)
390
+ attribs[:value] = nil
391
+ answer << user_set_nfs_home_directory(attribs, dir_info)
392
+ attribs[:value] = nil
363
393
  answer << user_enable_login(attribs, dir_info) if
364
394
  attribs[:enable]&.eql? 'true' or attribs[:enable]&.eql? true
365
395
  answer << user_disable_login(attribs, dir_info) unless
366
396
  attribs[:enable]&.eql? 'true' or attribs[:enable]&.eql? true
367
- attribs[:value] = nil
368
- answer << user_set_real_name(attribs, dir_info)
369
-
370
397
  return answer
371
398
  end
372
399
 
373
400
  # https://images.apple.com/server/docs/Command_Line.pdf
374
401
  # https://serverfault.com/questions/20702/how-do-i-create-user-accounts-from-the-terminal-in-mac-os-x-10-5?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa
375
402
  # https://superuser.com/questions/1154564/how-to-create-a-user-from-the-macos-command-line
376
- def user_create_full(attribs, dir_info)
403
+ def user_create(attribs, dir_info)
377
404
  attribs = user_record_name_alternatives(attribs)
378
405
 
379
406
  check_critical_attribute( attribs, :record_name )
@@ -383,24 +410,11 @@ module OpenDirectoryUtils
383
410
  answer = []
384
411
  attribs[:value] = nil
385
412
  answer << user_create_min(attribs, dir_info)
386
- attribs[:value] = nil
387
- answer << user_set_shell(attribs, dir_info)
388
413
  if attribs[:first_name] or attribs[:firstname] or attribs[:given_name] or
389
414
  attribs[:givenname]
390
415
  attribs[:value] = nil
391
416
  answer << user_set_first_name(attribs, dir_info)
392
417
  end
393
- if attribs[:last_name] or attribs[:lastname] or attribs[:sn] or
394
- attribs[:surname]
395
- attribs[:value] = nil
396
- answer << user_set_last_name(attribs, dir_info)
397
- end
398
- attribs[:value] = nil
399
- answer << user_set_unique_id(attribs, dir_info)
400
- attribs[:value] = nil
401
- answer << user_set_primary_group_id(attribs, dir_info)
402
- attribs[:value] = nil
403
- answer << user_set_nfs_home_directory(attribs, dir_info)
404
418
  # skip email if non-sent
405
419
  if attribs[:email] or attribs[:mail] or attribs[:apple_user_mailattribute]
406
420
  attribs[:value] = nil
@@ -418,7 +432,6 @@ module OpenDirectoryUtils
418
432
 
419
433
  return answer.flatten
420
434
  end
421
- alias_method :user_create, :user_create_full
422
435
 
423
436
  # dscl . -delete /Users/yourUserName
424
437
  # https://tutorialforlinux.com/2011/09/15/delete-users-and-groups-from-terminal/
@@ -434,49 +447,5 @@ module OpenDirectoryUtils
434
447
  dscl( user_attrs, dir_info )
435
448
  end
436
449
 
437
- # 1st keyword -- /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -create /Users/$shortname_USERNAME apple-keyword "$VALUE"
438
- # other keywords -- /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -append /Users/$shortname_USERNAME apple-keyword "$VALUE"
439
- def user_set_first_keyword
440
- end
441
-
442
- # /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -append /Users/$shortname_USERNAME apple-keyword "$VALUE"
443
- def user_append_keyword
444
- end
445
-
446
- # /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -create /Users/$shortname_USERNAME apple-company "$VALUE"
447
- def user_set_company
448
- end
449
- alias_method :las_program_info, :user_set_company
450
-
451
- # first - /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -create /Users/$USER apple-imhandle "$VALUE"
452
- # others - /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -append /Users/$USER apple-imhandle "$VALUE"
453
- # /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -create /Users/$USER apple-imhandle "AIM:created: $CREATE"
454
- # /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -append /Users/$USER apple-imhandle "ICQ:start: $START"
455
- # /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -append /Users/$USER apple-imhandle "MSN:end: $END"
456
- def user_set_chat
457
- end
458
- alias_method :user_set_chat_channels, :user_set_chat
459
- alias_method :las_created_date, :user_set_chat
460
- alias_method :las_start_date, :user_set_chat
461
- alias_method :las_end_date, :user_set_chat
462
-
463
- # /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -create /Users/$shortname_USERNAME apple-webloguri "$VALUE"
464
- def user_set_blog
465
- end
466
- alias_method :user_set_weblog, :user_set_blog
467
- alias_method :las_sync_date, :user_set_blog
468
-
469
- # /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -create /Users/$shortname_USERNAME apple-organizationinfo "$VALUE"
470
- def user_set_org_info
471
- end
472
- alias_method :las_set_organizational_info, :user_set_org_info
473
- alias_method :las_link_student_to_parent, :user_set_org_info
474
-
475
- # /usr/bin/dscl -u diradmin -P A-B1g-S3cret /LDAPv3/127.0.0.1 -create /Users/$shortname_USERNAME apple-relationships "$VALUE"
476
- def user_set_relationships
477
- end
478
- alias_method :las_link_parent_to_student, :user_set_relationships
479
-
480
-
481
450
  end
482
451
  end
@@ -1,22 +1,20 @@
1
1
  require 'net/ssh'
2
- # require "open_directory_utils/dscl"
3
- # require "open_directory_utils/pwpolicy"
4
2
  require "open_directory_utils/commands_base"
5
- require "open_directory_utils/commands_group"
6
- require "open_directory_utils/commands_user_attribs_od"
7
- require "open_directory_utils/commands_user_attribs_ldap"
3
+ require "open_directory_utils/commands_user_attribs"
4
+ # require "open_directory_utils/commands_user_attribs_ldap"
5
+ require "open_directory_utils/commands_user_create_remove"
6
+ require "open_directory_utils/commands_group_create_remove"
8
7
 
9
8
  module OpenDirectoryUtils
10
9
  class Connection
11
10
 
12
11
  attr_reader :srv_info, :dir_info
13
12
 
14
- # include OpenDirectoryUtils::Dscl
15
- # include OpenDirectoryUtils::Pwpolicy
16
13
  include OpenDirectoryUtils::CommandsBase
17
- include OpenDirectoryUtils::CommandsGroup
18
- include OpenDirectoryUtils::CommandsUserAttribsOd
19
- include OpenDirectoryUtils::CommandsUserAttribsLdap
14
+ include OpenDirectoryUtils::CommandsUserAttribs
15
+ # include OpenDirectoryUtils::CommandsUserAttribsLdap
16
+ include OpenDirectoryUtils::CommandsUserCreateRemove
17
+ include OpenDirectoryUtils::CommandsGroupCreateRemove
20
18
 
21
19
  # configure connection with ENV_VARS (or parameters)
22
20
  # @params [Hash] - reqiured info includes: srv_hostname:, srv_username: (password: if not using ssh-keys)
@@ -51,12 +49,12 @@ module OpenDirectoryUtils
51
49
  params[:record_name] = nil
52
50
  ssh_cmds = send(command, params, dir_info)
53
51
  # pp ssh_cmds
54
- results = send_cmds_to_od_server(ssh_cmds)
52
+ results = send_cmds_to_od_server(ssh_cmds)
55
53
  # pp results
56
- process_results(results, command, params, ssh_cmds)
54
+ answer = process_results(results, command, params, ssh_cmds )
55
+ return answer
57
56
  rescue ArgumentError, NoMethodError => error
58
- {error: {response: error.message, command: command,
59
- attributes: params, dscl_cmds: ssh_cmds}}
57
+ format_results(error.message, command, params, ssh_cmds, success: false)
60
58
  end
61
59
 
62
60
  private
@@ -74,82 +72,91 @@ module OpenDirectoryUtils
74
72
  end
75
73
 
76
74
  def process_results(results, command, params, ssh_cmds)
77
- results_str = results.to_s
78
- errors = true if results_str.include? 'Error'
79
- errors = false unless results_str.include? 'Error'
80
-
81
75
  if command.eql?(:user_exists?) or command.eql?(:group_exists?)
82
- found = record_found?(results_str)
83
- results = [ found, results ]
84
- return format_results(results, command, params, ssh_cmds, false)
76
+ return report_existence(results, command, params, ssh_cmds)
85
77
  end
86
-
87
- if results_str.include?('Group not found') or # can't find group to move user into
88
- results.to_s.include?('eDSRecordNotFound') or # return error if resource wasn't found
89
- results_str.include?('Record was not found') or # can't find user to move into a group
90
- results.to_s.include?('eDSAuthAccountDisabled') or # can't set passwd when disabled
91
- results_str.include?('unknown AuthenticationAuthority') # can't reset password when account disabled
92
- return format_results(results, command, params, ssh_cmds, true)
78
+ if missing_resource?(results)
79
+ results = ["Resource not found", results]
80
+ # report lack of success
81
+ return format_results(results, command, params, ssh_cmds, success: false)
93
82
  end
94
-
95
83
  if command.eql?(:user_password_verified?) or command.eql?(:user_password_ok?)
96
- passed = password_verified?(results_str)
97
- results = [ passed, results ]
98
- return format_results(results, command, params, ssh_cmds, false)
84
+ return report_password_check(results, command, params, ssh_cmds)
99
85
  end
100
-
101
86
  if command.eql?(:user_login_enabled?)
102
- # puts "login enabled -- #{results}".upcase
103
- enabled = login_enabled?(results_str)
104
- results = [ enabled, results ]
105
- return format_results(results, command, params, ssh_cmds, false)
87
+ return report_login_check(results, command, params, ssh_cmds)
106
88
  end
107
-
108
- if command.eql?(:user_in_group?) or command.eql?(:group_has_user?)
109
- username = params[:value]
110
- unless username.nil? or username.eql? '' or username.include? ' ' or
111
- results_str.include?('eDSRecordNotFound')
112
- results = [true, results] if results_str.include?( username )
113
- results = [false, results] unless results_str.include?( username )
114
- end
89
+ if command.eql?(:user_in_group?)
90
+ return report_in_group(results, command, params, ssh_cmds)
91
+ end
92
+ if results.to_s.include? 'attribute status: eDSNoStdMappingAvailable'
93
+ results = ["Bad Attribute Description", results]
94
+ format_results(results, command, params, ssh_cmds, success: false)
115
95
  end
96
+ if missed_errors?(results)
97
+ results = ["Unexpected Error", results]
98
+ format_results(results, command, params, ssh_cmds, success: false)
99
+ end
100
+ # return any general success answers
101
+ format_results(results, command, params, ssh_cmds, success: true)
102
+ end
116
103
 
117
- if errors and ( results_str.include?('eDSRecordNotFound') or
118
- results_str.include?('unknown AuthenticationAuthority') )
119
- results = ["Resource not found", results]
104
+ def format_results(results, command, params, ssh_cmds, success:)
105
+ ssh_clean = ssh_cmds.to_s
106
+ ssh_clean = ssh_clean.gsub(/-[p] ".+"/, '-p "************"')
107
+ ssh_clean = ssh_clean.gsub(/-[P] ".+"/, '-P "************"')
108
+
109
+ case success
110
+ when true
111
+ return { success:{response: results, command: command,
112
+ attributes: params, dscl_cmds: ssh_clean} }
113
+ else
114
+ return { error: {response: results, command: command,
115
+ attributes: params, dscl_cmds: ssh_clean} }
120
116
  end
117
+ end
121
118
 
122
- return format_results(results, command, params, ssh_cmds, errors)
119
+ def report_existence(results, command, params, ssh_cmds)
120
+ results = [false, results] if results.to_s.include?('eDSRecordNotFound')
121
+ results = [true, results] unless results.to_s.include?('eDSRecordNotFound')
122
+ return format_results(results, command, params, ssh_cmds, success: true)
123
+ end
123
124
 
125
+ def missing_resource?(results)
126
+ results_str = results.to_s
127
+ return true if results_str.include?('Group not found') or # can't find group to move user into
128
+ results_str.include?('eDSRecordNotFound') or # return error if resource wasn't found
129
+ results_str.include?('Record was not found') or # can't find user to move into a group
130
+ results_str.include?('eDSAuthAccountDisabled') or # can't set passwd when disabled
131
+ results_str.include?('unknown AuthenticationAuthority') # can't reset password when account disabled
132
+ return false
124
133
  end
125
134
 
126
- def format_results(results, command, params, ssh_cmds, errors)
127
- answer = case errors
128
- when false
129
- {success:{response: results, command: command, attributes: params}}
130
- else
131
- {error: {response: results, command: command,
132
- attributes: params, dscl_cmds: ssh_cmds}}
133
- end
134
- return answer
135
+ def report_password_check(results, command, params, ssh_cmds)
136
+ results = [false, results] if results.to_s.include?('eDSAuthFailed')
137
+ results = [true, results] unless results.to_s.include?('eDSAuthFailed')
138
+ return format_results(results, command, params, ssh_cmds, success: true)
135
139
  end
136
140
 
137
- def login_enabled?(results_str)
138
- return false if results_str.include?('account is disabled')
139
- return false if results_str.include?('isDisabled=1')
140
- # some enabled accounts return no policies ?#$?
141
- # return true if results_str.include?('isDisabled=0')
142
- true
141
+ def missed_errors?(results)
142
+ results_str = results.to_s
143
+ return true if results_str.include? 'Error'
144
+ return false
143
145
  end
144
146
 
145
- def password_verified?(results_str)
146
- return false if results_str.include?('eDSAuthFailed')
147
- true
147
+ def report_login_check(results, command, params, ssh_cmds)
148
+ results = [false, results] if results.to_s.include?('isDisabled=1')
149
+ results = [false, results] if results.to_s.include?('account is disabled')
150
+ results = [true, results] unless results.to_s.include?('isDisabled=1') or
151
+ results.to_s.include?('account is disabled')
152
+ return format_results(results, command, params, ssh_cmds, success: true)
148
153
  end
149
154
 
150
- def record_found?(results_str)
151
- return false if results_str.include?('eDSRecordNotFound')
152
- true
155
+ def report_in_group(results, command, params, ssh_cmds)
156
+ username = params[:value]
157
+ results = [true, results] if results.to_s.include?( username )
158
+ results = [false, results] unless results.to_s.include?( username )
159
+ return format_results(results, command, params, ssh_cmds, success: true)
153
160
  end
154
161
 
155
162
  def defaults
@@ -1,5 +1,5 @@
1
1
  module OpenDirectoryUtils
2
2
  module Version
3
- VERSION = "0.1.2"
3
+ VERSION = "0.1.3"
4
4
  end
5
5
  end
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.summary = %q{A ruby wrapper to access MacOpenDirectory management commands remotely}
13
13
  spec.description = %q{Create and update users and groups on a MacOpenDirectory Server}
14
- spec.homepage = "https://github.com/btihen/open_directory_utils"
14
+ spec.homepage = "https://github.com/LAS-IT/open_directory_utils"
15
15
  spec.license = "MIT"
16
16
 
17
17
  # # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: open_directory_utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bill Tihen
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2018-06-09 00:00:00.000000000 Z
12
+ date: 2018-06-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: net-ssh
@@ -92,15 +92,13 @@ files:
92
92
  - lib/open_directory_utils.rb
93
93
  - lib/open_directory_utils/clean_check.rb
94
94
  - lib/open_directory_utils/commands_base.rb
95
- - lib/open_directory_utils/commands_group.rb
96
- - lib/open_directory_utils/commands_user_attribs_ldap.rb
97
- - lib/open_directory_utils/commands_user_attribs_od.rb
95
+ - lib/open_directory_utils/commands_group_create_remove.rb
96
+ - lib/open_directory_utils/commands_user_attribs.rb
97
+ - lib/open_directory_utils/commands_user_create_remove.rb
98
98
  - lib/open_directory_utils/connection.rb
99
- - lib/open_directory_utils/dscl.rb
100
- - lib/open_directory_utils/pwpolicy.rb
101
99
  - lib/open_directory_utils/version.rb
102
100
  - open_directory_utils.gemspec
103
- homepage: https://github.com/btihen/open_directory_utils
101
+ homepage: https://github.com/LAS-IT/open_directory_utils
104
102
  licenses:
105
103
  - MIT
106
104
  metadata: {}