s3-passdb Add support for pdb_add_sam_account() and password hashes to pdb_samba4
[amitay/samba.git] / source3 / passdb / pdb_samba4.c
index aa3352a65887a130c19c4eaaf0d9e7633c3e010e..35316d71d4bdf0236fd739b8c6a9e84fde5c4677 100644 (file)
@@ -328,115 +328,224 @@ static bool pdb_samba4_add_time(struct ldb_message *msg,
        return ldb_msg_add_fmt(msg, attrib, "%llu", (unsigned long long) nt_time);
 }
 
-/* Like in pdb_ldap(), this will need to be a function pointer when we
- * start to support 'adds' for migrations from samba3 passdb backends
- * to samba4 */
-static bool update_required(struct samu *sam, enum pdb_elements element)
-{
-       return (IS_SAM_CHANGED(sam, element));
-}
-
-static bool pdb_samba4_init_samba4_from_sam(struct pdb_samba4_state *state,
-                                           struct ldb_message *existing,
-                                           TALLOC_CTX *mem_ctx,
-                                           struct ldb_message **pmods, 
-                                           struct samu *sam)
+static int pdb_samba4_replace_by_sam(struct pdb_samba4_state *state,
+                                    bool (*need_update)(const struct samu *,
+                                                        enum pdb_elements),
+                                    struct ldb_dn *dn,
+                                    struct samu *sam)
 {
        int ret = LDB_SUCCESS;
        const char *pw;
        struct ldb_message *msg;
-
+       struct ldb_request *req;
+       unsigned int j;
        /* TODO: All fields :-) */
 
-       msg = ldb_msg_new(mem_ctx);
+       msg = ldb_msg_new(talloc_tos());
        if (!msg) {
                return false;
        }
 
-       msg->dn = existing->dn;
+       msg->dn = dn;
+
+       /* build modify request */
+       ret = ldb_build_mod_req(&req, state->ldb, talloc_tos(), msg, NULL, NULL,
+                               NULL, NULL);
+        if (ret != LDB_SUCCESS) {
+               talloc_free(msg);
+               return ret;
+        }
 
        pw = pdb_get_plaintext_passwd(sam);
-       if (update_required(sam, PDB_PLAINTEXT_PW)) {
+       if (need_update(sam, PDB_PLAINTEXT_PW)) {
                if (pw == NULL) {
-                       ret = LDB_ERR_OPERATIONS_ERROR;
-                       goto fail;
+                       return LDB_ERR_OPERATIONS_ERROR;
                }
                
                ret |= ldb_msg_add_string(msg, "clearTextPassword", pw);
+       } else {
+               bool changed_lm_pw = false;
+               bool changed_nt_pw = false;
+               bool changed_history = false;
+               if (need_update(sam, PDB_LMPASSWD)) {
+                       struct ldb_val val;
+                       val.data = pdb_get_lanman_passwd(sam);
+                       if (!val.data) {
+                               samdb_msg_add_delete(state->ldb, msg, msg,
+                                                    "dBCSPwd");
+                       } else {
+                               val.length = LM_HASH_LEN;
+                               ret |= ldb_msg_add_value(msg, "dBCSPwd", &val, NULL);
+                       }
+                       changed_lm_pw = true;
+               }
+               if (need_update(sam, PDB_NTPASSWD)) {
+                       struct ldb_val val;
+                       val.data = pdb_get_lanman_passwd(sam);
+                       if (!val.data) {
+                               samdb_msg_add_delete(state->ldb, msg, msg,
+                                                    "unicodePwd");
+                       } else {
+                               val.length = NT_HASH_LEN;
+                               ret |= ldb_msg_add_value(msg, "unicodePwd", &val, NULL);
+                       }
+                       changed_nt_pw = true;
+               }
+
+               /* Try to ensure we don't get out of sync */
+               if (changed_lm_pw && !changed_nt_pw) {
+                       samdb_msg_add_delete(state->ldb, msg, msg,
+                                            "unicodePwd");
+               } else if (changed_nt_pw && !changed_lm_pw) {
+                       samdb_msg_add_delete(state->ldb, msg, msg,
+                                            "dBCSPwd");
+               }
+               if (changed_lm_pw || changed_nt_pw) {
+                       samdb_msg_add_delete(state->ldb, msg, msg,
+                                            "supplementalCredentials");
+
+               }
+
+               /* If we set a plaintext password, the system will
+                * force the pwdLastSet to now(), and it isn't worth
+                * working around this for the real world use cases of
+                * pdb_samba4 */
+               if (need_update(sam, PDB_PASSLASTSET)) {
+                       ret |= pdb_samba4_add_time(msg, "pwdLastSet",
+                                                  pdb_get_pass_last_set_time(sam));
+               }
+
+               if (need_update(sam, PDB_PWHISTORY)) {
+                       uint32_t current_hist_len;
+                       const uint8_t *history = pdb_get_pw_history(sam, &current_hist_len);
+
+                       bool invalid_history = false;
+                       struct samr_Password *history_hashes = talloc_array(talloc_tos(), struct samr_Password,
+                                                                           current_hist_len);
+                       if (!history) {
+                               invalid_history = true;
+                       } else {
+                               unsigned int i;
+                               static const uint8_t zeros[16];
+                               /* Parse the history into the correct format */
+                               for (i = 0; i < current_hist_len; i++) {
+                                       if (memcmp(&history[i*PW_HISTORY_ENTRY_LEN], zeros, 16) != 0) {
+                                               /* If the history is in the old format, with a salted hash, then we can't migrate it to AD format */
+                                               invalid_history = true;
+                                               break;
+                                       }
+                                       /* Copy out the 2nd 16 bytes of the 32 byte password history, containing the NT hash */
+                                       memcpy(history_hashes[i].hash,
+                                              &history[(i*PW_HISTORY_ENTRY_LEN) + PW_HISTORY_SALT_LEN],
+                                              sizeof(history_hashes[i].hash));
+                               }
+                       }
+                       if (invalid_history) {
+                               ret |= samdb_msg_add_delete(state->ldb, msg, msg,
+                                                    "ntPwdHistory");
+
+                               ret |= samdb_msg_add_delete(state->ldb, msg, msg,
+                                                    "lmPwdHistory");
+                       } else {
+                               ret |= samdb_msg_add_hashes(state->ldb, msg, msg,
+                                                           "ntPwdHistory",
+                                                           history_hashes,
+                                                           current_hist_len);
+                       }
+                       changed_history = true;
+               }
+               if (changed_lm_pw || changed_nt_pw || changed_history) {
+                       ret |= ldb_request_add_control(req,
+                                                      DSDB_CONTROL_BYPASS_PASSWORD_HASH_OID,
+                                                      true, NULL);
+               }
        }
 
-       if (update_required(sam, PDB_FULLNAME)) {
+       /* PDB_USERSID is only allowed on ADD, handled in caller */
+       if (need_update(sam, PDB_GROUPSID)) {
+               const struct dom_sid *sid = pdb_get_group_sid(sam);
+               uint32_t rid;
+               NTSTATUS status = dom_sid_split_rid(NULL, sid, NULL, &rid);
+               if (!NT_STATUS_IS_OK(status)) {
+                       return LDB_ERR_OPERATIONS_ERROR;
+               }
+               if (!dom_sid_in_domain(samdb_domain_sid(state->ldb), sid)) {
+                       return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX;
+               }
+               ret |= samdb_msg_add_uint(state->ldb, msg, msg, "primaryGroupID", rid);
+       }
+       if (need_update(sam, PDB_FULLNAME)) {
                ret |= ldb_msg_add_string(msg, "displayName", pdb_get_fullname(sam));
        }
 
-       if (update_required(sam, PDB_SMBHOME)) {
+       if (need_update(sam, PDB_SMBHOME)) {
                ret |= ldb_msg_add_string(msg, "homeDirectory",
                                          pdb_get_homedir(sam));
        }
 
-       if (update_required(sam, PDB_PROFILE)) {
+       if (need_update(sam, PDB_PROFILE)) {
                ret |= ldb_msg_add_string(msg, "profilePath",
                                          pdb_get_profile_path(sam));
        }
 
-       if (update_required(sam, PDB_DRIVE)) {
+       if (need_update(sam, PDB_DRIVE)) {
                ret |= ldb_msg_add_string(msg, "homeDrive",
                                          pdb_get_dir_drive(sam));
        }
 
-       if (update_required(sam, PDB_LOGONSCRIPT)) {
+       if (need_update(sam, PDB_LOGONSCRIPT)) {
                ret |= ldb_msg_add_string(msg, "scriptPath",
                                          pdb_get_logon_script(sam));
        }
 
-       if (update_required(sam, PDB_KICKOFFTIME)) {
+       if (need_update(sam, PDB_KICKOFFTIME)) {
                ret |= pdb_samba4_add_time(msg, "accountExpires",
                                        pdb_get_kickoff_time(sam));
        }
 
-       if (update_required(sam, PDB_USERNAME)) {
+       if (need_update(sam, PDB_USERNAME)) {
                ret |= ldb_msg_add_string(msg, "samAccountName",
                                          pdb_get_username(sam));
        }
 
-       if (update_required(sam, PDB_HOURSLEN) || update_required(sam, PDB_HOURS)) {
+       if (need_update(sam, PDB_HOURSLEN) || need_update(sam, PDB_HOURS)) {
                struct ldb_val hours = data_blob_const(pdb_get_hours(sam), pdb_get_hours_len(sam));
                ret |= ldb_msg_add_value(msg, "logonHours",
                                         &hours, NULL);
        }
 
-       if (update_required(sam, PDB_ACCTCTRL)) {
-               ret |= ldb_msg_add_fmt(msg, "userAccountControl",
-                                      "%d", ds_acb2uf(pdb_get_acct_ctrl(sam)));
+       if (need_update(sam, PDB_ACCTCTRL)) {
+               ret |= samdb_msg_add_acct_flags(state->ldb, msg, msg,
+                                               "userAccountControl", pdb_get_acct_ctrl(sam));
        }
 
-       if (update_required(sam, PDB_COMMENT)) {
+       if (need_update(sam, PDB_COMMENT)) {
                ret |= ldb_msg_add_string(msg, "comment",
                                          pdb_get_comment(sam));
        }
 
-       if (update_required(sam, PDB_ACCTDESC)) {
+       if (need_update(sam, PDB_ACCTDESC)) {
                ret |= ldb_msg_add_string(msg, "description",
                                          pdb_get_acct_desc(sam));
        }
 
-       if (update_required(sam, PDB_WORKSTATIONS)) {
+       if (need_update(sam, PDB_WORKSTATIONS)) {
                ret |= ldb_msg_add_string(msg, "userWorkstations",
                                          pdb_get_workstations(sam));
        }
 
        /* This will need work, it is actually a UTF8 'string' with internal NULLs, to handle TS parameters */
-       if (update_required(sam, PDB_MUNGEDDIAL)) {
+       if (need_update(sam, PDB_MUNGEDDIAL)) {
                ret |= ldb_msg_add_string(msg, "userParameters",
                                          pdb_get_munged_dial(sam));
        }
 
-       if (update_required(sam, PDB_COUNTRY_CODE)) {
+       if (need_update(sam, PDB_COUNTRY_CODE)) {
                ret |= ldb_msg_add_fmt(msg, "countryCode",
                                       "%i", (int)pdb_get_country_code(sam));
        }
 
-       if (update_required(sam, PDB_CODE_PAGE)) {
+       if (need_update(sam, PDB_CODE_PAGE)) {
                ret |= ldb_msg_add_fmt(msg, "codePage",
                                       "%i", (int)pdb_get_code_page(sam));
        }
@@ -445,28 +554,46 @@ static bool pdb_samba4_init_samba4_from_sam(struct pdb_samba4_state *state,
        PDB_LOGONTIME,
        PDB_LOGOFFTIME,
        PDB_BAD_PASSWORD_TIME,
-       PDB_CANCHANGETIME,
-       PDB_MUSTCHANGETIME,
+       PDB_CANCHANGETIME, - these are calculated per policy, not stored
+       PDB_MUSTCHANGETIME, - these are calculated per policy, not stored
        PDB_DOMAIN,
-       PDB_NTUSERNAME,
+       PDB_NTUSERNAME, - this makes no sense, and never really did
        PDB_LOGONDIVS,
-       PDB_USERSID,
-       PDB_GROUPSID,
-       PDB_PASSLASTSET,
+       PDB_USERSID, - Handled in pdb_samba4_add_sam_account()
        PDB_FIELDS_PRESENT,
        PDB_BAD_PASSWORD_COUNT,
        PDB_LOGON_COUNT,
        PDB_UNKNOWN6,
-       PDB_LMPASSWD,
-       PDB_NTPASSWD,
        PDB_PWHISTORY,
        PDB_BACKEND_PRIVATE_DATA,
 
  */
+       if (ret != LDB_SUCCESS) {
+               return LDB_ERR_OPERATIONS_ERROR;
+       }
 
-       *pmods = msg;
-fail:
-       return ret == LDB_SUCCESS;
+       if (msg->num_elements == 0) {
+               /* Nothing to do, just return success */
+               return LDB_SUCCESS;
+       }
+
+       /* mark everything here as a replace */
+       for (j=0;j<msg->num_elements;j++) {
+               msg->elements[j].flags = LDB_FLAG_MOD_REPLACE;
+       }
+
+       ret = ldb_request(state->ldb, req);
+       if (ret == LDB_SUCCESS) {
+               ret = ldb_wait(req->handle, LDB_WAIT_ALL);
+       }
+
+       if (ret != LDB_SUCCESS) {
+               DEBUG(0,("Failed to modify account record %s to set user attributes: %s\n",
+                        ldb_dn_get_linearized(msg->dn),
+                        ldb_errstring(state->ldb)));
+       }
+
+       return ret;
 }
 
 static NTSTATUS pdb_samba4_getsamupriv(struct pdb_samba4_state *state,
@@ -626,7 +753,50 @@ static NTSTATUS pdb_samba4_delete_user(struct pdb_methods *m,
 static NTSTATUS pdb_samba4_add_sam_account(struct pdb_methods *m,
                                        struct samu *sampass)
 {
-       return NT_STATUS_NOT_IMPLEMENTED;
+       int ret;
+       NTSTATUS status;
+       struct ldb_dn *dn;
+       struct pdb_samba4_state *state = talloc_get_type_abort(
+               m->private_data, struct pdb_samba4_state);
+       uint32_t acb_flags = pdb_get_acct_ctrl(sampass);
+       const char *username = pdb_get_username(sampass);
+       const struct dom_sid *user_sid = pdb_get_user_sid(sampass);
+       TALLOC_CTX *tframe = talloc_stackframe();
+
+       acb_flags &= (ACB_NORMAL|ACB_WSTRUST|ACB_SVRTRUST|ACB_DOMTRUST);
+
+       ret = ldb_transaction_start(state->ldb);
+       if (ret != LDB_SUCCESS) {
+               talloc_free(tframe);
+               return NT_STATUS_LOCK_NOT_GRANTED;
+       }
+
+       status = dsdb_add_user(state->ldb, talloc_tos(), username,
+                              acb_flags, user_sid, NULL, &dn);
+       if (!NT_STATUS_IS_OK(status)) {
+               ldb_transaction_cancel(state->ldb);
+               talloc_free(tframe);
+               return status;
+       }
+
+       ret = pdb_samba4_replace_by_sam(state, pdb_element_is_set_or_changed,
+                                       dn, sampass);
+       if (ret != LDB_SUCCESS) {
+               ldb_transaction_cancel(state->ldb);
+               talloc_free(tframe);
+               return dsdb_ldb_err_to_ntstatus(ret);
+       }
+
+       ret = ldb_transaction_commit(state->ldb);
+       if (ret != LDB_SUCCESS) {
+               DEBUG(0,("Failed to commit transaction to add and modify account record %s: %s\n",
+                        ldb_dn_get_linearized(dn),
+                        ldb_errstring(state->ldb)));
+               talloc_free(tframe);
+               return NT_STATUS_INTERNAL_DB_CORRUPTION;
+       }
+       talloc_free(tframe);
+       return NT_STATUS_OK;
 }
 
 /*
@@ -642,28 +812,11 @@ static NTSTATUS pdb_samba4_update_sam_account(struct pdb_methods *m,
                m->private_data, struct pdb_samba4_state);
        struct ldb_message *msg = pdb_samba4_get_samu_private(
                m, sam);
-       struct ldb_message *replace_msg;
-       int rc;
-
-       if (!pdb_samba4_init_samba4_from_sam(state, msg, talloc_tos(),
-                                            &replace_msg, sam)) {
-               return NT_STATUS_NO_MEMORY;
-       }
-
-       if (replace_msg->num_elements == 0) {
-               /* Nothing to do, just return success */
-               return NT_STATUS_OK;
-       }
-
-       rc = dsdb_replace(state->ldb, replace_msg, 0);
-       TALLOC_FREE(replace_msg);
-       if (rc != LDB_SUCCESS) {
-               DEBUG(10, ("dsdb_replace for %s failed: %s\n", ldb_dn_get_linearized(replace_msg->dn),
-                          ldb_errstring(state->ldb)));
-               return NT_STATUS_LDAP(rc);
-       }
+       int ret;
 
-       return NT_STATUS_OK;
+       ret = pdb_samba4_replace_by_sam(state, pdb_element_is_changed, msg->dn,
+                                       sam);
+       return dsdb_ldb_err_to_ntstatus(ret);
 }
 
 static NTSTATUS pdb_samba4_delete_sam_account(struct pdb_methods *m,