6.1: Secure Accounts Manager

The SAM Database stores and manages all user and group account information and their inter-relationships. Surprisingly, users include "Trust" Accounts, which is similar in concept to NIS+ requiring that all servers be included in the NIS+ database: SAM requires that all servers be listed in the Domain, too.

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.

No relation to Unix
Yes, it's true. A SAM database implementation has NOTHING to do with a Unix Database. That includes one implemented on Unix.

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.

Name space Conventions
Usernames, alias names, group names and Trusted Domain names must all be unique. It is NOT possible to have a user named root and a group named root in a SAM database. A SAM database implementor MUST NOT allow this to occur, as it will cause either the user or the group to become unselectable by name, depending on the implementation!

Low-level library sharing
There is a surprisingly small amount of code that needs to be shared between SAM database implementations. It includes:

se_access_check
use of SeAccessCheck to implement security (see equivalent function in Microsoft's MSDN)

DCE/RPC marshalling / unmarshalling routines
These are hidden from the developer, as is the norm for all DCE/RPC application development: TNG is no exception. However, you must still link your application with the DCE/RPC framework! The advantage is that this framework does not change from one SAM database implementation to another.

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.

DCE/RPC handle / context routines
It is possible to store long-lived connection state information and to have that information passed back to you by another remote DCE/RPC function. The two relevant functions are the rather obtuse set_policy_state() and get_policy_state_info() functions. These will become transparent at some point in the future when we move to a full IDL compiler back-end: they represent, in a very stupid way, the [handle] IDL specifier.

Useful Utility Routines
To make the job of maintaining several SAM database implementations, some implementers may choose not to support quite as many sub-levels (known as Info Levels) as are available. We intend to provide generic conversion routines that will assist with this [primarily because they have to exist in some SAM implementations anyway]

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:

SAM is comprehensive
The SAM API is comprehensive and complete, not complex. A full list of the prototypes, divided by section, is listed below. To attempt to map the SAM api to some other API in order to attempt to hide from the number and nature of the functions that SAM provides simply makes the other API an additional burden, with possible omissions, additional complexity and the associated maintenance headaches.

Bite the bullet: implement the whole damn lot, and do it as simply and directly as possible.

SAM strings are stored in UNICODE-16.
Some database implementors may choose to treat this as binary data. Some may choose to convert to UTF-8, others to Ascii. Some may have native UNICODE-16 library support, or may choose to write their own UNICODE-16 library (which must have case-insensitive search capabilities).

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.

NTTIME format
SAM timestamps are typically NTTIME, a 64-bit time in 100ns resolution ticks since Jan 1st 1600. A similar argument applies here as to strings: let the developer choose the most suitable format.
This, and the UNICODE-16 string issue, implies that any low-level utility functions that TNG provides, should use the same formats as those offered by SAM - NOT the Unix time_t (1 second resolution since Jan 1st 1970) and NOT Ascii or UTF-8, as if that restriction is applied, then some applications may have to convert BACK to NTTIME and UNICODE-16, which is at best a waste of time and at worse a serious and potentially critical loss of information.

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:

LOCK
Lock the database

RETRIEVE
Retrieve SAM_USER_INFO_21

MERGE
Into the data retrieved, modify (merge) a part of it

STORE
Store SAM_USER_INFO_21

UNLOCK
Unlock the database

In the case of the TDB User SAM database instance, this becomes, as is seen above:

LOCK
tdb_lock

RETRIEVE
tdb_lookup_user

MERGE
usr.acb_info = acb_info

STORE
tdb_store_user

UNLOCK
tdb_unlock

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):

LOCK
LOCK sam_user WRITE;

RETRIEVE
SELECT * from sam_user WHERE user_rid = 1000;

MERGE
usr.acb_info = acb_info

STORE
UPDATE sam_user SET user_name='...', full_name='...', ... pw_last_set_time='...', group_rid=592, ... WHERE user_rid = 1000;

UNLOCK
UNLOCK sam_user;
The considerations of maintaining consistency on the SAM name space are not shown here, either, for clarity. A proper SQL database, such as PostgresQL, can have a combined foriegn key constraint on the user name in the sam_user table, group name in the sam_group table, alias name in the sam_alias table etc. that will alleviate the necessity to perform programmatic locking and manual calculation of the name space uniqueness. Messy, in other words.

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:

SAM boot-strapping
The issue of pre-loading a SAM database with the default entries is of some concern. The initial Administrator and Guest users and all the BUILTIN member groups do not exist until they are created, and until they are created, it is not possible to perform any authorisation - including the right to add an Administrator account!

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).

SAM editing
Some SAM database implementations may not allow convenient editing except using samedit or USRMGR.EXE. Certainly, the TDB implementation will not, as the data is stored purely in binary format.

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.