Active Directory Directory foundations

Create and validate a custom attribute

Add a custom attribute to computer accounts, work through the LDIF imports and check that the application can use it.

Work out what the application needs

Say your inventory application needs an equipment code on computer accounts. Before adding an attribute, work out the format, who owns the value, who will read and write it, how often it changes, and what should happen when an account is renamed, moved or deleted. An existing attribute might fit, but check what it means and whether another application already uses it.

Here, exampleDeviceCode is an optional, single-valued, case-insensitive Unicode string of 1–32 characters. Equipment codes can have leading zeroes and punctuation, so we’ll use a string. We’ll also leave it unindexed and out of the GC unless the application needs those features.

The schema checks the type, number of values and length. The application still has to check the business format, such as AU-00427, handle duplicate codes and synchronize with the inventory system. Adding an ordinary index won’t enforce uniqueness. Attribute design, index semantics.

There are four pieces to set up

Layer What this example changes What to check
Attribute definition Adds exampleDeviceCode to the forest schema Syntax, identity and replicated definition
Class applicability Adds an optional auxiliary class to computer Existing and future computer objects allow the attribute
Access control Uses the target object’s effective ACL Intended writer succeeds; other identities are appropriately denied
Object data Populates selected computer accounts Correct value reaches the application and required replicas

Once you’ve created the attributeSchema object, you still need to allow the attribute on a class. You can add it directly to an existing class’s mayContain. We’ll use an auxiliary class to keep the application’s optional attributes together. Creating a subclass of computer wouldn’t convert existing computer accounts into that subclass. Extension choices.

Fix the identifiers before the first installation

Allocate an OID for the attribute (attributeID) and another for the auxiliary class (governsID) under a namespace you control. Microsoft prefers a registered root from a naming authority, and also documents a GUID-derived root-generation method for extensions in your own forests where external parties do not require registration. The latter produces an unregistered identifier, not an assigned registered namespace. An OID printed in someone else’s example is not an allocation to your application.

Each definition also needs its own schemaIDGUID. Generate these once for the extension and keep them with its OIDs and LDIF files. Use the same values in test and production forests and in later releases: object-specific ACEs can refer to them. AD can generate an omitted GUID, but supplying your own keeps it consistent across installations. This schema GUID is separate from an individual directory object’s objectGUID. Attribute identifiers.

This generates two GUIDs locally and converts their bytes to the base64 form LDIF needs:

$attributeGuid = [guid]::NewGuid()
$classGuid = [guid]::NewGuid()
[pscustomobject]@{
    AttributeGuid = $attributeGuid
    AttributeGuidBase64 = [Convert]::ToBase64String($attributeGuid.ToByteArray())
    ClassGuid = $classGuid
    ClassGuidBase64 = [Convert]::ToBase64String($classGuid.ToByteArray())
}

Save these values with the OIDs, CNs, LDAP names, syntax, range, flags, class association, intended ACL and versioned LDIF. Don’t generate fresh GUIDs each time the installer runs.

Inspect the actual schema master and current definitions

Start in an isolated test forest you can throw away. Before a deployment, check replication, access to the writable schema master and your tested recovery plan. Schema changes affect the whole forest, and uninstalling the application won’t let you delete its definitions. Schema extension boundaries.

You’ll normally use an account in Schema Admins, though the required schema operations can be delegated. The RootDSE cache-refresh records also need Update Schema Cache control access on the contacted DC’s nTDSDSA object or the schema-NC root. With limited delegation, the attribute might be created successfully while the cache refresh fails. Check both results. Cache operation and access check.

With RSAT, these commands read the selected forest and candidate names:

Import-Module ActiveDirectory
$forest = Get-ADForest -Server 'dc1.contoso.com'
$schemaMaster = $forest.SchemaMaster
$schema = (Get-ADRootDSE -Server $schemaMaster).schemaNamingContext
Get-ADObject -Server $schemaMaster -SearchBase $schema -SearchScope OneLevel `
    -LDAPFilter '(|(cn=Example-Device-Code)(cn=Example-Device-Inventory)(lDAPDisplayName=exampleDeviceCode)(lDAPDisplayName=exampleDeviceInventory))' `
    -Properties lDAPDisplayName,attributeID,governsID,schemaIDGUID,isDefunct |
    Format-List Name,lDAPDisplayName,attributeID,governsID,schemaIDGUID,isDefunct

Check the allocated OIDs and schema GUIDs across both classes and attributes too. If a definition already exists, compare all its fields with your saved design before proceeding. Don’t skip an import error just because the name looks right. Save the current definition of computer, including its auxiliary classes.

Stage the LDIF in dependency order

These templates change the schema. Before importing them in the lab, replace every angle-bracket placeholder with the identifiers you’ve allocated. Use the schema DN returned by RootDSE for the schema-container suffix. It follows the forest root, which might differ from your account’s domain. Leave the blank dn: records blank; those address RootDSE.

Save each stage as its own file with a final newline. The double colon on schemaIDGUID:: means base64 data. Avoid copying server-maintained export fields such as distinguishedName, objectCategory or uSNChanged into a new-object recipe. LDIF schema examples and dependency handling.

01-attribute.ldf: create the definition and refresh the local cache

dn: CN=Example-Device-Code,CN=Schema,CN=Configuration,DC=contoso,DC=com
changetype: add
objectClass: attributeSchema
cn: Example-Device-Code
adminDisplayName: Example device code
lDAPDisplayName: exampleDeviceCode
attributeID: <ATTRIBUTE_OID>
schemaIDGUID:: <BASE64_ATTRIBUTE_GUID>
attributeSyntax: 2.5.5.12
oMSyntax: 64
isSingleValued: TRUE
rangeLower: 1
rangeUpper: 32
searchFlags: 0
isMemberOfPartialAttributeSet: FALSE
description: Optional equipment code owned by the device inventory application

dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-

2.5.5.12 with oMSyntax: 64 is String(Unicode), with case-insensitive matching. No oMObjectClass value is needed for this syntax. A 32-character limit applies to a populated value; making the attribute optional lets an account omit it entirely.

02-auxiliary-class.ldf: group the optional attribute

dn: CN=Example-Device-Inventory,CN=Schema,CN=Configuration,DC=contoso,DC=com
changetype: add
objectClass: classSchema
cn: Example-Device-Inventory
adminDisplayName: Example device inventory
lDAPDisplayName: exampleDeviceInventory
governsID: <CLASS_OID>
schemaIDGUID:: <BASE64_CLASS_GUID>
objectClassCategory: 3
subClassOf: top
mayContain: exampleDeviceCode

dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-

objectClassCategory: 3 defines an auxiliary class. All its attributes here are optional. AD’s schema safety checks reject adding new mandatory attributes to an existing class, including an auxiliary association that introduces such a requirement. Design the extension to preserve existing object-creation workflows, and enforce inventory completeness in the application. Schema modification safety checks.

03-associate-computer.ldf: make it available on computer accounts

dn: CN=Computer,CN=Schema,CN=Configuration,DC=contoso,DC=com
changetype: modify
add: auxiliaryClass
auxiliaryClass: exampleDeviceInventory
-

dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-

Use add so the existing auxiliary classes stay in place. This static association makes the attributes available to every instance of computer and its subclasses. It doesn’t write any attribute values to those objects. There’s a slightly confusing detail here: the auxiliary-class name won’t appear in each computer object’s objectClass list. Check the class definition and allowed attributes to see whether the association worked. Static versus dynamic auxiliary classes.

Import one stage, inspect it, then continue

Schema change: from the folder containing your lab LDIF files, use the account with schema-change rights. Create the protected log directory first:

ldifde -i -f 01-attribute.ldf -s schema-master.contoso.com -j C:\SchemaLab\Logs -v

Use the actual schema master’s hostname. Check the exit status, log and new definition before importing stages 02 and 03 with their corresponding filenames. If a later record fails, LDIFDE doesn’t undo the records it already applied.

The LDIFDE command reference defines these switches. If an installer uses -c for portability, it performs global string replacement; inspect the whole result rather than assuming it rewrites only DNs. -n omits binary values during export and is not an import dry run. Preserve the base64 schema identities when preparing a deployment file.

Avoid adding -k to get past a failed rerun. It skips a broader set of errors, including ones you’ll want to investigate. Compare the partially applied stage with your saved design before continuing. Microsoft’s schema LDIF guidance explains the error handling. For an OS schema upgrade, use the supported Adprep procedure.

Each schemaUpdateNow write refreshes the contacted DC’s cache. It is blocking and local, and does not deliver missing replication to another DC. Validate the definition and cache readiness where the application will connect. Schema-cache behavior.

Put a value on an account and read it back

After the extension is ready on a selected writable DC, this changes one pre-existing lab computer account:

$dc = 'dc1.contoso.com'
Set-ADComputer -Server $dc -Identity 'LABPC01' `
    -Replace @{exampleDeviceCode = 'AU-00427'} -Confirm
Get-ADComputer -Server $dc -Identity 'LABPC01' -Properties exampleDeviceCode |
    Select-Object DistinguishedName,ObjectGUID,exampleDeviceCode

Test with the application service identity as well as the schema administrator. Class applicability grants no per-object write permission. Resolve the attribute’s schemaIDGUID when reviewing attribute-specific ACEs and inherited property rights; see schema and ACL interpretation. Broad existing Read Property permissions may expose a new ordinary attribute, so access requirements belong in the design before values are stored.

On another intended replica, compare the schema definition, then read the same object GUID and explicit property. The schema definition and the object’s new value might arrive at different times. Test the application’s searches using its own identity and endpoint too. We left this attribute out of the GC, so read the value through the owning domain’s normal LDAP view. GC query diagnosis.

Lab check Expected evidence
Existing account and newly created computer Both can hold the optional value after schema/cache readiness
33-character value or multiple values Rejected by the selected schema constraints
Missing optional value Account remains valid; the application handles absence
Intended writer and an unauthorized identity Effective ACL produces the planned success/denial
Rename or OU move Same object identity retains the code; the application follows its identity
Second writable DC Matching definition and eventually matching object value, with server/time recorded
Code lookup and duplicate code Matching semantics are understood; application handles duplicates rather than assuming index-enforced uniqueness

Exercise the expected rejection cases only against lab objects. Record returned errors and distinguish schema constraint failures from permissions, stale caches and application validation.

Plan retirement before deployment

Work out how to stop the application writing, what to do with existing values, and which class associations, ACLs and other applications still depend on the extension. Making a custom definition defunct won’t erase old instances. Check those dependencies and stored values before removing an auxiliary-class association too. Defunct schema behavior.

If the design needs to change, it’s much easier to start again in a disposable forest. In an established forest, you’ll need to plan around the schema definitions already installed. For an attribute that refers to other objects, continue with designing a forward link and backlink.

← All Active Directory topics