TNG has six separate SAM database implementations, with the LDAP one and the legacy smbpasswd one in production usage at the time of writing. Four of these SAM databases are available as a configure option in one daemon - samrd, whilst the other three are under development: a TDB implementation (samrtdbd) and an LDAP implementation that conforms to the Windows 2000 LDAP SAM Schema (samrnt5ldapd).
TODO: The SAM database is effectively an informational endpoint.
This section outlines what the requirements are for implementing a SAM database under the TNG architecture. It also includes a description of a program, sam2sam, which will allow administrators to convert a SAM database in one format to another format.
This mistake was made in the original samrd design, attempting to mirror the unix /etc/passwd and /etc/group (etc.) database in the SAM by actually reading /etc/group (etc.) entries and presenting them as the SAM database.
Whoops.
It is SURS's job to provide the linkage between SAM accounts and unix accounts, and that does not mean inside a SAM database implementation. Nowhere inside a SAM database is it necessary to access Unix account information. A SAM stores NT passwords, NT accounts by RID, Aliases etc. There is no room for Unix passwords or unix user ids in a SAM implementation, and absolutely no need, either.
The SAM application developer, however, writes the _samr_*() routines, and it is the job of the DCE/RPC framework to call, and to hide the mechanics of the calling of, the server-side _samr_*() functions, from the application developer.
Conversion considerations
It has been mentioned that it is preferred that an API be created that hides the complexity of SAM development from the developer. There are a number of points that need to be clarified:
Bite the bullet: implement the whole damn lot, and do it as simply and directly as possible.
To impose any specific string format is a restriction and yet another programmatic step. So why bother? Let the developer choose the most suitable format.
SAM API
Here is the list of SAM routines that must be implemented. A tiny percentage of these routines are not well-defined: they have had to be recreated from over-the-wire data over a long period of observation and experimentation. Consequently, they may change, although the SAM API is one of the most complete and comprehensive of the TNG codebase.
/*The following definitions come from samrd/srv_samr_passdb.c */ /* * sam management */ uint32 _samr_close(POLICY_HND * hnd); uint32 _samr_connect_anon(const UNISTR2 * srv_name, uint32 access_mask, POLICY_HND * connect_pol); uint32 _samr_connect(const UNISTR2 * srv_name, uint32 access_mask, POLICY_HND * connect_pol); uint32 _samr_unknown_2d(const POLICY_HND * domain_pol, const DOM_SID * sid); uint32 _samr_query_sec_obj(const POLICY_HND * user_pol, SEC_DESC_BUF * buf); uint32 _samr_enum_dom_users(const POLICY_HND * pol, uint32 * start_idx, uint16 acb_mask, uint16 unk_1, uint32 size, SAM_ENTRY ** sam, UNISTR2 ** uni_acct_name, uint32 * num_sam_users); uint32 _samr_enum_domains(const POLICY_HND * pol, uint32 * start_idx, uint32 size, SAM_ENTRY ** sam, UNISTR2 ** uni_acct_name, uint32 * num_sam_users); uint32 _samr_enum_dom_groups(const POLICY_HND * pol, uint32 * start_idx, uint32 size, SAM_ENTRY ** sam, UNISTR2 ** uni_acct_name, uint32 * num_sam_groups); uint32 _samr_enum_dom_aliases(const POLICY_HND * pol, uint32 * start_idx, uint32 size, SAM_ENTRY ** sam, UNISTR2 ** uni_acct_name, uint32 * num_sam_aliases); uint32 _samr_query_dispinfo(const POLICY_HND * domain_pol, uint16 level, uint32 start_idx, uint32 max_entries, uint32 max_size, uint32 * data_size, uint32 * num_entries, SAM_DISPINFO_CTR * ctr); uint32 _samr_lookup_names(const POLICY_HND * pol, uint32 num_names1, uint32 flags, uint32 ptr, const UNISTR2 * uni_name, uint32 * num_rids1, uint32 rid[MAX_SAM_ENTRIES], uint32 * num_types1, uint32 type[MAX_SAM_ENTRIES]); uint32 _samr_lookup_rids(const POLICY_HND * pol, uint32 num_rids, uint32 flags, const uint32 * rids, uint32 * num_names, UNIHDR ** hdr_name, UNISTR2 ** uni_name, uint32 ** types); /* * user password changing */ uint32 _samr_chgpasswd_user(const UNISTR2 * uni_dest_host, const UNISTR2 * uni_user_name, const char nt_newpass[516], const uchar nt_oldhash[16], const char lm_newpass[516], const uchar lm_oldhash[16]); uint32 _samr_get_dom_pwinfo(const UNISTR2 * uni_srv_name, uint16 * unk_0, uint16 * unk_1); uint32 _samr_get_usrdom_pwinfo(const POLICY_HND * user_pol, uint16 * unknown_0, uint16 * unknown_1, uint32 * unknown_2); /* * domain management */ uint32 _samr_lookup_domain(const POLICY_HND * connect_pol, const UNISTR2 * uni_domain, DOM_SID * dom_sid); uint32 _samr_open_domain(const POLICY_HND * connect_pol, uint32 ace_perms, const DOM_SID * sid, POLICY_HND * domain_pol); uint32 _samr_query_dom_info(const POLICY_HND * domain_pol, uint16 switch_value, SAM_UNK_CTR * ctr); /* * group management */ uint32 _samr_add_groupmem(const POLICY_HND * pol, uint32 rid, uint32 unknown); uint32 _samr_del_groupmem(const POLICY_HND * pol, uint32 rid); uint32 _samr_delete_dom_group(POLICY_HND * group_pol); uint32 _samr_query_groupmem(const POLICY_HND * group_pol, uint32 * num_mem, uint32 ** rid, uint32 ** attr); uint32 _samr_set_groupinfo(const POLICY_HND * pol, uint16 switch_level, const GROUP_INFO_CTR * ctr); uint32 _samr_query_groupinfo(const POLICY_HND * pol, uint16 switch_level, GROUP_INFO_CTR * ctr); uint32 _samr_create_dom_group(const POLICY_HND * domain_pol, const UNISTR2 * uni_acct_name, uint32 access_mask, POLICY_HND * group_pol, uint32 * rid); uint32 _samr_open_group(const POLICY_HND * domain_pol, uint32 access_mask, uint32 group_rid, POLICY_HND * group_pol); /* * alias management */ uint32 _samr_query_aliasinfo(const POLICY_HND * alias_pol, uint16 switch_level, ALIAS_INFO_CTR * ctr); uint32 _samr_delete_dom_alias(POLICY_HND * alias_pol); uint32 _samr_query_aliasmem(const POLICY_HND * alias_pol, uint32 * num_mem, DOM_SID2 ** sid); uint32 _samr_delete_dom_user(POLICY_HND *user_pol); uint32 _samr_add_aliasmem(const POLICY_HND * alias_pol, const DOM_SID * sid); uint32 _samr_del_aliasmem(const POLICY_HND * alias_pol, const DOM_SID * sid); uint32 _samr_create_dom_alias(const POLICY_HND * domain_pol, const UNISTR2 * uni_acct_name, uint32 access_mask, POLICY_HND * alias_pol, uint32 * rid); uint32 _samr_open_alias(const POLICY_HND * domain_pol, uint32 access_mask, uint32 alias_rid, POLICY_HND * alias_pol); /* * user management */ uint32 _samr_open_user(const POLICY_HND * domain_pol, uint32 access_mask, uint32 user_rid, POLICY_HND * user_pol); uint32 _samr_query_userinfo(const POLICY_HND * pol, uint16 switch_value, SAM_USERINFO_CTR * ctr); uint32 _samr_set_userinfo(const POLICY_HND * pol, uint16 switch_value, SAM_USERINFO_CTR * ctr); uint32 _samr_set_userinfo2(const POLICY_HND * pol, uint16 switch_value, SAM_USERINFO_CTR * ctr); uint32 _samr_create_user(const POLICY_HND * domain_pol, const UNISTR2 * uni_username, uint16 acb_info, uint32 access_mask, POLICY_HND * user_pol, uint32 * unknown_0, uint32 * user_rid); uint32 _samr_query_usergroups(const POLICY_HND * pol, uint32 * num_groups, DOM_GID ** gids); uint32 _samr_query_useraliases(const POLICY_HND * pol, const uint32 * ptr_sid, const DOM_SID2 * sid, uint32 * num_aliases, uint32 ** rid);As can be seen from this list, unlike NIS+ and nssswitch, it's not just getpwent(), getgrent(), it's get, set, enumerate, add, delete and sliced bread and the toaster, too. It has to be said: if Microsoft had decided to publish this as an RFC when Windows NT 3.1 was first released, it would have stood a good chance of becoming the world-wide standard method of account management, even if it is limited to the NT security model.
6.1.1: Useful Utility Routines
To save individual implementors a considerable amount of time, and to make maintenance easier, they MAY wish to use some utility routines that merge info levels. These merge routines take the info level with the most data members as input and output, and merge info levels with less data members into it.
This is best explained by example. Let us take the TDB implementation. This stores an NDR (network data representation) binary blob associated with a RID. In the User SAM-TDB, the blob that is stored is a SAM_USER_INFO_21 structure:
/* SAM_USER_INFO_21 */
typedef struct sam_user_info_21
{
NTTIME logon_time; /* logon time */
NTTIME logoff_time; /* logoff time */
NTTIME pass_last_set_time; /* password last set time */
NTTIME kickoff_time; /* kickoff time */
NTTIME pass_can_change_time; /* password can change time */
NTTIME pass_must_change_time; /* password must change time */
UNIHDR hdr_user_name; /* username unicode string header */
UNIHDR hdr_full_name; /* user's full name unicode string header */
UNIHDR hdr_home_dir; /* home directory unicode string header */
UNIHDR hdr_dir_drive; /* home drive unicode string header */
UNIHDR hdr_logon_script; /* logon script unicode string header */
UNIHDR hdr_profile_path; /* profile path unicode string header */
UNIHDR hdr_acct_desc ; /* user description */
UNIHDR hdr_workstations; /* comma-separated workstations user can log in from */
UNIHDR hdr_unknown_str ; /* don't know what this is, yet. */
UNIHDR hdr_munged_dial ; /* munged path name and dial-back tel number */
uint8 lm_pwd[16]; /* lm user passwords */
uint8 nt_pwd[16]; /* nt user passwords */
uint32 user_rid; /* Primary User ID */
uint32 group_rid; /* Primary Group ID */
uint32 acb_info; /* account info (ACB_xxxx bit-mask) */
uint32 unknown_3; /* 0x00ff ffff */
uint16 logon_divs; /* 0x0000 00a8 which is 168 which is num hrs in a week */
/* uint8 pad[2] */
uint32 ptr_logon_hrs; /* unknown pointer */
uint32 unknown_5; /* 0x0002 0000 */
uint8 padding1[8];
UNISTR2 uni_user_name; /* username unicode string */
UNISTR2 uni_full_name; /* user's full name unicode string */
UNISTR2 uni_home_dir; /* home directory unicode string */
UNISTR2 uni_dir_drive; /* home directory drive unicode string */
UNISTR2 uni_logon_script; /* logon script unicode string */
UNISTR2 uni_profile_path; /* profile path unicode string */
UNISTR2 uni_acct_desc ; /* user description unicode string */
UNISTR2 uni_workstations; /* login from workstations unicode string */
UNISTR2 uni_unknown_str ; /* don't know what this is, yet. */
UNISTR2 uni_munged_dial ; /* munged path name and dial-back tel number */
uint32 unknown_6; /* 0x0000 04ec */
uint32 padding4;
LOGON_HRS logon_hrs;
} SAM_USER_INFO_21;
Using NDR is inspired by the format of the SAM Windows
Registry Hive. NDR is an extremely convenient means to
easily store and retrieve large, dynamic-sized data structures
in a flat binary format. Here are two other SAM_USER_INFO
structures - info levels 7 (user name) and 10 (ACB bits)
respectively:
/* SAM_USER_INFO_7 */
typedef struct sam_user_info_7
{
UNIHDR hdr_user_name;
UNISTR2 uni_user_name;
} SAM_USER_INFO_7;
/* SAM_USER_INFO_10 */
typedef struct sam_user_info_10
{
uint32 acb_info;
} SAM_USER_INFO_10;
So, the procedure that must be followed to modify a small part of a single user entry in the database is outlined by this very simple code fragment:
static BOOL tdb_set_userinfo_10(TDB_CONTEXT * tdb, uint16 acb_info)
{
SAM_USER_INFO_21 usr;
if (tdb_lockall(tdb) != 0)
{
return False;
}
if (!tdb_lookup_user(tdb, &usr))
{
tdb_unlockall(tdb);
return False;
}
usr.acb_info = acb_info;
if (!tdb_store_user(tdb, &usr))
{
tdb_unlockall(tdb);
return False;
}
tdb_unlockall(tdb);
return True;
}
The procedure is:
In the case of the TDB User SAM database instance, this becomes, as is seen above:
Now, given that there are approximately thirty known SAM_USER_INFO info levels, it makes inordinately large amounts of sense to split the merging into a single function, which in the case of SAM_USER_INFO, has the following prototype:
/* merge takes a sam user info container (ctr) with the info
* level named in info_level, and modifies the appropriate
* member variables of the usr parameter
*/
status_t sam_user_info_21_merge(SAM_USER_INFO_21 *usr,
const SAM_USER_INFO_CTR *ctr,
int info_level);
/* split takes a sam user info 21 (usr), and creates the appropriate
* member variables in the ctr parameter, at the info level
* specified in info_level
*/
status_t sam_user_info_21_split(const SAM_USER_INFO_21 *usr,
SAM_USER_INFO_CTR *ctr,
int info_level);
The purpose of this function is to hide the number of
info levels, as some of them are not yet known.
SQL Database considerations
In the case of a (My)SQL database, which only has full table locking (no row locking), and also has no foriegn key constraints, the following statements will need to be made are listed here. The full number of fields in the UPDATE statement are not listed, for clarity. The example is where the ACB info for User Rid 1000 is being modified (Set on SAM_USER_INFO_10):
Now, the difference between using TDB (or any other Berkeley-like database) and SQL is that SQL has columns, and TDB, gdbm and the Windows Registry etc. do not. This imposes a requirement on a TDB-ish design - if implementing a SAM database in a sane way - to use the lock / retrieve / merge / store / unlock procedure. However, on column-level database implementations such as SQL, the only requirement to use the merge procedure is for simplicity and maintainability.
If a SAM database implementor chooses not to use the advised merge procedure, that is entirely their choice. For example, assuming a PostgresQL or other database with the appropriate foriegn key constraints, here is an example SQL statement to perform exactly the same as the above SAM_USER_INFO_10 set:
UPDATE sam_user SET acb_info = NNNN;
Wow. What A Big Deal. Except that, the implementor will need to provide approximately fifty such UPDATE statements, all told, instead of about four or five.
Additionally, fifty or so SELECT statements will be required to obtain all the various info levels, instead of four or five followed by calling one conversion routine to create the requested info level from the larger one.
Other SAM Data Structures
The other data structures which need to be similarly treated are SAM_DOMAIN_INFO, SAM_GROUP_INFO, SAM_ALIAS_INFO and SAM_DISPLAY_INFO, all of which have several info levels, not all of which are, at present, known or supported. Each of these will require a merge routine and a convert routine.
6.1.2: sam2sam - SAM conversion utility
sam2sam is a proposed program that will read in an entire SAM database in one format and convert it to another format. The means by which this is achieved is that sam2sam takes two parameters - the name of an input SAM dynamic library, and the name of an output SAM dynamic library. sam2sam will then enumerate all domain, user, group and alias information using the input SAM library, and call the appropriate create routines in the output SAM library.
For this to work, there must be a common SAM API to work from. The most sensible API to use for this task is the _sam_*() API, listed above (see 6.1), as this allows for complete, comprehensive and simple data access, as outlined above.
There is a possible alternative approach to the design of sam2sam. ntdom-netlogon has a function, NetSamSync, by which it is possible to perform a backup of an entire SAM database. There is no reason why the slurp-half of sam2sam should not use this function, other than it doesn't help with the store-half. For these reasons, sam2sam is best kept simple and symmetrical in its design, instead of using a NETLOGON dynamic library for input and a sam dynamic library for output - especially as the implementation of NETLOGON calls the _sam_*() api directly itself, anyway.
So, in this way, sam2sam can be used to convert from any available SAM database format, such as a flat smbpasswd file format, to any other SAM database format, such as (My)SQL, LDAP - anything. So, this helps in two ways:
sam2sam can be used to pre-load a pre-defined SAM database in a flat-file format with the minimum required entries, and to store it in the preferred SAM format. That preferred format may be yet again in flat-file format, but in a different location.
The advantage of using sam2sam to perform this operation, instead of copying the flat-file template to the live location is that file locking would be handled correctly, (which would be mandatory in any flat-file-based SAM implementation, for data integrity).
The use of sam2sam to convert any database format into a convenient flat-file format, followed by editing, followed by conversion back - whilst TNG is running (live) - will be extremely powerful. and slightly dangerous.