xref: /illumos-gate/usr/src/lib/smbsrv/libmlsvc/common/mlsvc_util.c (revision f40487698292848b93d33bbc76f92ca1c063e39e)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 /*
22  * Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
23  * Copyright 2018 Nexenta Systems, Inc.  All rights reserved.
24  * Copyright 2021 RackTop Systems, Inc.
25  */
26 
27 /*
28  * Utility functions to support the RPC interface library.
29  */
30 
31 #include <stdio.h>
32 #include <stdarg.h>
33 #include <strings.h>
34 #include <unistd.h>
35 #include <netdb.h>
36 #include <stdlib.h>
37 #include <sys/time.h>
38 #include <sys/systm.h>
39 #include <note.h>
40 #include <syslog.h>
41 
42 #include <smbsrv/libsmb.h>
43 #include <smbsrv/libsmbns.h>
44 #include <smbsrv/libmlsvc.h>
45 #include <smb/ntaccess.h>
46 #include <smbsrv/smbinfo.h>
47 #include <smbsrv/netrauth.h>
48 #include <libsmbrdr.h>
49 #include <lsalib.h>
50 #include <samlib.h>
51 #include <mlsvc.h>
52 
53 static DWORD
54 mlsvc_join_rpc(smb_domainex_t *dxi,
55     char *admin_user, char *admin_pw,
56     char *machine_name, char *machine_pw);
57 static DWORD
58 mlsvc_join_noauth(smb_domainex_t *dxi,
59     char *machine_name, char *machine_pw);
60 
61 /*
62  * This is called by smbd_dc_update just after we've learned about a
63  * new domain controller.  Make sure we can authenticate with this DC.
64  */
65 DWORD
66 mlsvc_netlogon(char *server, char *domain)
67 {
68 	DWORD status;
69 
70 	status = smb_netlogon_check(server, domain);
71 	if (status != NT_STATUS_SUCCESS) {
72 		syslog(LOG_NOTICE, "Failed to establish NETLOGON "
73 		    "credential chain with DC: %s (%s)", server,
74 		    xlate_nt_status(status));
75 		syslog(LOG_NOTICE, "The machine account information on the "
76 		    "domain controller does not match the local storage.");
77 		syslog(LOG_NOTICE, "To correct this, use 'smbadm join'");
78 	}
79 
80 	return (status);
81 }
82 
83 /*
84  * Join the specified domain.  The method varies depending on whether
85  * we're using "secure join" (using an administrative account to join)
86  * or "unsecure join" (using a pre-created machine account).  In the
87  * latter case, the machine account is created "by hand" before this
88  * machine attempts to join, and we just change the password from the
89  * (weak) default password for a new machine account to a random one.
90  *
91  * Returns NT status codes.
92  */
93 void
94 mlsvc_join(smb_joininfo_t *info, smb_joinres_t *res)
95 {
96 	static unsigned char zero_hash[SMBAUTH_HASH_SZ];
97 	char machine_name[SMB_SAMACCT_MAXLEN];
98 	char machine_pw[NETR_MACHINE_ACCT_PASSWD_MAX];
99 	unsigned char passwd_hash[SMBAUTH_HASH_SZ];
100 	char addrbuf[INET6_ADDRSTRLEN];
101 	smb_domainex_t dxi;
102 	smb_domain_t *di = &dxi.d_primary;
103 	DWORD status;
104 	int rc;
105 
106 	bzero(&dxi, sizeof (dxi));
107 
108 	/*
109 	 * Domain join support: AD (Kerberos+LDAP) or MS-RPC?
110 	 */
111 	boolean_t ads_enabled = smb_config_get_ads_enable();
112 
113 	if (smb_getsamaccount(machine_name, sizeof (machine_name)) != 0) {
114 		res->status = NT_STATUS_INVALID_COMPUTER_NAME;
115 		return;
116 	}
117 
118 	(void) smb_gen_random_passwd(machine_pw, sizeof (machine_pw));
119 
120 	/*
121 	 * Ensure that any previous membership of this domain has
122 	 * been cleared from the environment before we start. This
123 	 * will ensure that we don't attempt a NETLOGON_SAMLOGON
124 	 * when attempting to find the PDC.
125 	 */
126 	(void) smb_config_setbool(SMB_CI_DOMAIN_MEMB, B_FALSE);
127 
128 	if (info->domain_username[0] != '\0') {
129 		(void) smb_auth_ntlm_hash(info->domain_passwd, passwd_hash);
130 		smb_ipc_set(info->domain_username, passwd_hash);
131 		syslog(LOG_INFO, "smbd: joining with user %s",
132 		    info->domain_username);
133 	} else {
134 		smb_ipc_set(MLSVC_ANON_USER, zero_hash);
135 		syslog(LOG_INFO, "smbd: joining with anonymous");
136 	}
137 
138 	/*
139 	 * Tentatively set the idmap domain to the one we're joining,
140 	 * so that the DC locator in idmap knows what to look for.
141 	 * Ditto the SMB server domain.
142 	 */
143 	if (smb_config_set_idmap_domain(info->domain_name) != 0)
144 		syslog(LOG_NOTICE, "Failed to set idmap domain name");
145 	if (smb_config_refresh_idmap() != 0)
146 		syslog(LOG_NOTICE, "Failed to refresh idmap service");
147 	syslog(LOG_INFO, "smbd: set idmap domain %s", info->domain_name);
148 
149 	/* Clear DNS local (ADS) lookup cache. */
150 	smb_ads_refresh(B_FALSE);
151 
152 	/*
153 	 * Locate a DC for this domain.  Intentionally bypass the
154 	 * ddiscover service here because we're still joining.
155 	 * This also allows better reporting of any failures.
156 	 */
157 	status = smb_ads_lookup_msdcs(info->domain_name, &dxi.d_dci);
158 	if (status != NT_STATUS_SUCCESS) {
159 		syslog(LOG_ERR,
160 		    "smbd: failed to locate AD server for domain %s (%s)",
161 		    info->domain_name, xlate_nt_status(status));
162 		goto out;
163 	}
164 
165 	/*
166 	 * Found a DC.  Report what we found along with the return status
167 	 * so that admin will know which AD server we were talking to.
168 	 */
169 	(void) strlcpy(res->dc_name, dxi.d_dci.dc_name, MAXHOSTNAMELEN);
170 	if (smb_inet_ntop(&dxi.d_dci.dc_addr,
171 	    addrbuf, sizeof (addrbuf)) == NULL)
172 		strcpy(addrbuf, "?");
173 	syslog(LOG_INFO, "smbd: found AD server %s (%s)",
174 	    dxi.d_dci.dc_name, addrbuf);
175 
176 	/*
177 	 * Domain discovery needs to authenticate with the AD server.
178 	 * Disconnect any existing connection with the domain controller
179 	 * to make sure we won't use any prior authentication context
180 	 * our redirector might have.
181 	 */
182 	mlsvc_disconnect(dxi.d_dci.dc_name);
183 
184 	/*
185 	 * Get the domain policy info (domain SID etc).
186 	 * Here too, bypass the smb_ddiscover_service.
187 	 */
188 	status = smb_ddiscover_main(info->domain_name, &dxi);
189 	if (status != NT_STATUS_SUCCESS) {
190 		syslog(LOG_ERR,
191 		    "smbd: failed getting domain info for %s (%s)",
192 		    info->domain_name, xlate_nt_status(status));
193 		goto out;
194 	}
195 	/*
196 	 * After a successful smbd_ddiscover_main() call
197 	 * we should call smb_domain_save() to update the
198 	 * data shown by smbadm list.  Do that at the end,
199 	 * only if all goes well with joining the domain.
200 	 */
201 
202 	/*
203 	 * Log info about the domain from ddiscover
204 	 */
205 	syslog(LOG_INFO, "smbd_join: domain FQN=%s", di->di_fqname);
206 	syslog(LOG_INFO, "smbd_join: domain NBN=%s", di->di_nbname);
207 	syslog(LOG_INFO, "smbd_join: domain SID=%s", di->di_sid);
208 
209 	/*
210 	 * Create or update our machine account on the DC.
211 	 * A non-null user means we do "secure join".
212 	 */
213 	if (info->domain_username[0] != '\0') {
214 		/*
215 		 * If enabled, try to join using AD Services.
216 		 */
217 		status = NT_STATUS_UNSUCCESSFUL;
218 		if (ads_enabled) {
219 			syslog(LOG_INFO, "use_ads=true (LDAP join)");
220 			res->join_err = smb_ads_join(di->di_fqname,
221 			    info->domain_username, info->domain_passwd,
222 			    machine_pw);
223 			if (res->join_err == SMB_ADS_SUCCESS) {
224 				status = NT_STATUS_SUCCESS;
225 			}
226 		} else {
227 			syslog(LOG_INFO, "use_ads=false (RPC join)");
228 
229 			/*
230 			 * If ADS was disabled, join using RPC.
231 			 */
232 			status = mlsvc_join_rpc(&dxi,
233 			    info->domain_username,
234 			    info->domain_passwd,
235 			    machine_name, machine_pw);
236 		}
237 
238 	} else {
239 		/*
240 		 * Doing "Unsecure join" (pre-created account)
241 		 */
242 		status = mlsvc_join_noauth(&dxi, machine_name, machine_pw);
243 	}
244 
245 	if (status != NT_STATUS_SUCCESS)
246 		goto out;
247 
248 	/*
249 	 * Make sure we can authenticate using the
250 	 * (new, or updated) machine account.
251 	 */
252 	(void) smb_auth_ntlm_hash(machine_pw, passwd_hash);
253 	smb_ipc_set(machine_name, passwd_hash);
254 	rc = smbrdr_logon(dxi.d_dci.dc_name, di->di_nbname, machine_name);
255 	if (rc != 0) {
256 		syslog(LOG_NOTICE, "Authenticate with "
257 		    "new/updated machine account: %s",
258 		    strerror(rc));
259 		res->join_err = SMB_ADJOIN_ERR_AUTH_NETLOGON;
260 		status = NT_STATUS_CANT_ACCESS_DOMAIN_INFO;
261 		goto out;
262 	}
263 
264 	/*
265 	 * Store the new machine account password, and
266 	 * SMB_CI_DOMAIN_MEMB etc.
267 	 */
268 	rc = smb_setdomainprops(NULL, dxi.d_dci.dc_name, machine_pw);
269 	if (rc != 0) {
270 		syslog(LOG_NOTICE,
271 		    "Failed to save machine account password");
272 		res->join_err = SMB_ADJOIN_ERR_STORE_PROPS;
273 		status = NT_STATUS_INTERNAL_DB_ERROR;
274 		goto out;
275 	}
276 
277 	/*
278 	 * Update idmap config?
279 	 * Already set the domain_name above.
280 	 */
281 
282 	/*
283 	 * Save the SMB server config.  Sets: SMB_CI_DOMAIN_*
284 	 * Should unify SMB vs idmap configs.
285 	 */
286 	smb_config_setdomaininfo(di->di_nbname, di->di_fqname,
287 	    di->di_sid,
288 	    di->di_u.di_dns.ddi_forest,
289 	    di->di_u.di_dns.ddi_guid);
290 	smb_ipc_commit();
291 	smb_domain_save();
292 
293 	status = 0;
294 
295 out:
296 
297 	if (status != 0) {
298 		/*
299 		 * Undo the tentative domain settings.
300 		 */
301 		(void) smb_config_set_idmap_domain("");
302 		(void) smb_config_refresh_idmap();
303 		smb_ipc_rollback();
304 	}
305 
306 	/* Avoid leaving cleartext passwords around. */
307 	bzero(machine_pw, sizeof (machine_pw));
308 	bzero(passwd_hash, sizeof (passwd_hash));
309 
310 	res->status = status;
311 }
312 
313 static DWORD
314 mlsvc_join_rpc(smb_domainex_t *dxi,
315     char *admin_user, char *admin_pw,
316     char *machine_name,  char *machine_pw)
317 {
318 	mlsvc_handle_t samr_handle;
319 	mlsvc_handle_t domain_handle;
320 	mlsvc_handle_t user_handle;
321 	smb_account_t ainfo;
322 	char *server = dxi->d_dci.dc_name;
323 	smb_domain_t *di = &dxi->d_primary;
324 	DWORD account_flags;
325 	DWORD rid;
326 	DWORD status;
327 	int rc;
328 
329 	/* Caller did smb_ipc_set() so we don't need the pw for now. */
330 	_NOTE(ARGUNUSED(admin_pw));
331 
332 	rc = samr_open(server, di->di_nbname, admin_user,
333 	    MAXIMUM_ALLOWED, &samr_handle);
334 	if (rc != 0) {
335 		syslog(LOG_NOTICE, "sam_connect to server %s failed", server);
336 		return (RPC_NT_SERVER_UNAVAILABLE);
337 	}
338 	/* have samr_handle */
339 
340 	status = samr_open_domain(&samr_handle, MAXIMUM_ALLOWED,
341 	    (struct samr_sid *)di->di_binsid, &domain_handle);
342 	if (status != NT_STATUS_SUCCESS)
343 		goto out_samr_handle;
344 	/* have domain_handle */
345 
346 	account_flags = SAMR_AF_WORKSTATION_TRUST_ACCOUNT;
347 	status = samr_create_user(&domain_handle, machine_name,
348 	    account_flags, &rid, &user_handle);
349 	if (status == NT_STATUS_USER_EXISTS) {
350 		status = samr_lookup_domain_names(&domain_handle,
351 		    machine_name, &ainfo);
352 		if (status != NT_STATUS_SUCCESS)
353 			goto out_domain_handle;
354 		status = samr_open_user(&domain_handle, MAXIMUM_ALLOWED,
355 		    ainfo.a_rid, &user_handle);
356 	}
357 	if (status != NT_STATUS_SUCCESS) {
358 		syslog(LOG_NOTICE,
359 		    "smbd: failed to open machine account (%s)",
360 		    xlate_nt_status(status));
361 		goto out_domain_handle;
362 	}
363 
364 	/*
365 	 * The account exists, and we have user_handle open
366 	 * on that account.  Set the password and flags.
367 	 */
368 
369 	status = netr_set_user_password(&user_handle, machine_pw);
370 	if (status != NT_STATUS_SUCCESS) {
371 		syslog(LOG_NOTICE,
372 		    "smbd: failed to set machine account password (%s)",
373 		    xlate_nt_status(status));
374 		goto out_user_handle;
375 	}
376 
377 	account_flags |= SAMR_AF_DONT_EXPIRE_PASSWD;
378 	status = netr_set_user_control(&user_handle, account_flags);
379 	if (status != NT_STATUS_SUCCESS) {
380 		syslog(LOG_NOTICE,
381 		    "Set machine account control flags: %s",
382 		    xlate_nt_status(status));
383 		goto out_user_handle;
384 	}
385 
386 out_user_handle:
387 	(void) samr_close_handle(&user_handle);
388 out_domain_handle:
389 	(void) samr_close_handle(&domain_handle);
390 out_samr_handle:
391 	(void) samr_close_handle(&samr_handle);
392 
393 	return (status);
394 }
395 
396 /*
397  * Doing "Unsecure join" (using a pre-created machine account).
398  * All we need to do is change the password from the default
399  * to a random string.
400  *
401  * Note: this is a work in progres.  Nexenta issue 11960
402  * (allow joining an AD domain using a pre-created computer account)
403  * It turns out that to change the machine account password,
404  * we need to use a different RPC call, performed over the
405  * NetLogon secure channel.  (See netr_server_password_set2)
406  */
407 static DWORD
408 mlsvc_join_noauth(smb_domainex_t *dxi,
409     char *machine_name, char *machine_pw)
410 {
411 	char old_pw[SMB_SAMACCT_MAXLEN];
412 	DWORD status;
413 
414 	/*
415 	 * Compose the current (default) password for the
416 	 * pre-created machine account, which is just the
417 	 * account name in lower case, truncated to 14
418 	 * characters.
419 	 */
420 	if (smb_gethostname(old_pw, sizeof (old_pw), SMB_CASE_LOWER) != 0)
421 		return (NT_STATUS_INTERNAL_ERROR);
422 	old_pw[14] = '\0';
423 
424 	status = netr_change_password(dxi->d_dci.dc_name, machine_name,
425 	    old_pw, machine_pw);
426 	if (status != NT_STATUS_SUCCESS) {
427 		syslog(LOG_NOTICE,
428 		    "Change machine account password: %s",
429 		    xlate_nt_status(status));
430 	}
431 	return (status);
432 }
433 
434 void
435 mlsvc_disconnect(const char *server)
436 {
437 	smbrdr_disconnect(server);
438 }
439 
440 /*
441  * A few more helper functions for RPC services.
442  */
443 
444 /*
445  * Check whether or not the specified user has administrator privileges,
446  * i.e. is a member of Domain Admins or Administrators.
447  * Returns true if the user is an administrator, otherwise returns false.
448  */
449 boolean_t
450 ndr_is_admin(ndr_xa_t *xa)
451 {
452 	smb_netuserinfo_t *ctx = xa->pipe->np_user;
453 
454 	return (ctx->ui_flags & SMB_ATF_ADMIN);
455 }
456 
457 /*
458  * Check whether or not the specified user has power-user privileges,
459  * i.e. is a member of Domain Admins, Administrators or Power Users.
460  * This is typically required for operations such as managing shares.
461  * Returns true if the user is a power user, otherwise returns false.
462  */
463 boolean_t
464 ndr_is_poweruser(ndr_xa_t *xa)
465 {
466 	smb_netuserinfo_t *ctx = xa->pipe->np_user;
467 
468 	return ((ctx->ui_flags & SMB_ATF_ADMIN) ||
469 	    (ctx->ui_flags & SMB_ATF_POWERUSER));
470 }
471 
472 int32_t
473 ndr_native_os(ndr_xa_t *xa)
474 {
475 	smb_netuserinfo_t *ctx = xa->pipe->np_user;
476 
477 	return (ctx->ui_native_os);
478 }
479