Field-level security
xrmverse can protect single columns: a secured column is readable and writable only by System Administrators and by the users and teams that a field security profile, or a field share on one record, lets in. The Microsoft documentation describes the feature in Column-level security to control access and Column-level security by using code. This section says what xrmverse does and where it stops. Whether each detail matches a real Dataverse is not verified (see the checklist).
Securing a column. AttributeMetadata.IsSecured is set with CreateAttribute or UpdateAttribute, the Web API
PUT EntityDefinitions(…)/Attributes(…), xrmd fieldsecurity secure|unsecure, the Secured switch of the web admin,
or a solution; it needs prvWriteAttribute, like any other change of a column. Turning IsSecured off also needs read,
create and update permission on that column (through profiles; System Administrators bypass), so a System Customizer cannot
lift the protection of a column it cannot read: otherwise 403 0x8004F508, with nothing removed. A solution import that
turns a secured column off is checked the same way. Every column definition reports
CanBeSecuredForCreate, CanBeSecuredForRead and CanBeSecuredForUpdate, computed by the server (a value you send is
ignored):
- Securable (all three true): String, Memo, Integer, BigInt, Decimal, Double, Money, Boolean, Picklist, MultiSelectPicklist, DateTime, File and Image columns, custom or system.
- Not securable (all three false): the primary id and primary name; Lookup, Customer and Owner columns;
statecode,statuscode,createdon,modifiedon,createdby,modifiedby,owneridandversionnumber; the companion columns of another column (*name,*yominame,*_base,*_url,*_timestamp); and every column of the five field security tables, ofauditand of the security tables (users, teams, roles, business units and their links).IsSecuredtrue on such a column is a 400 with code0x8004F501.
Turning IsSecured off removes the column's permissions from every profile and its field shares, and so does deleting the
column; deleting a table removes every permission naming it and the field shares of its records. A column created later
with the same name therefore starts with no grants. describe_table (MCP) marks secured columns with "secured": true.
Companion columns follow their column. A secured Money column's _base value, a secured File column's file name
(<column>_name) and a secured Image column's id, timestamp and url (<column>id, <column>_timestamp, <column>_url)
are protected with the rights of the column itself: they read as null (or are left out of the JSON), a filter, order or
aggregate on them is the same 403 0x8004F504, naming one in a Create or Update is the 403 of the column, and a field
share on the column covers them on that record. The fileattachment row of a secured File column, or of a secured
Image column's full-size image, holds the same details (file name, type, size): a caller whose profiles do not grant
read on the column gets no such row, whether it queries fileattachment directly, retrieves the row by id or links to it
from the record.
Who reads and writes a secured column.
- Everyone holding the System Administrator role (directly or through an owner team), the SYSTEM user and the server's own work: always, with or without a profile.
- Everyone else, System Customizer included: only what their profiles allow. A user's profiles are those given to the user plus those of every team the user belongs to (owner and access teams); per column the permissions add up, so one profile allowing read is enough. Under impersonation both the impersonator and the impersonated user must be allowed. A plug-in's service is checked as the user it runs as.
- Changes to profiles, permissions, profile assignments, team membership and
IsSecuredapply to the next request, with no restart.
Profiles and permissions. A profile is a fieldsecurityprofile row; each of its fieldpermission rows names a table
(entityname) and a secured column (attributelogicalname) and sets canread, cancreate and canupdate to 4
(Allowed) or 0 (Not Allowed, the default). A permission on a column that is not secured is a 400 (0x8004F50E), and
one on a table or column that does not exist a 404; the same holds when an Update moves a permission to another column.
Profiles are given to users and teams with Associate on systemuserprofiles_association and
teamprofiles_association (direct writes on the intersects systemuserprofiles and teamprofiles are refused), which
needs prvWriteFieldSecurityProfile. Deleting a profile deletes its permissions and its assignments. Only the System
Administrator role holds the profile and permission privileges; System Customizer and Basic User hold none, so they cannot
change profiles directly or through a solution import (see Solution).
The System Administrator profile (id 572329c1-a042-4e22-be47-367c6374ea45, from memory and not verified) is seeded
in every organization, has no permissions and cannot be changed or deleted, and no permission can be added to it (403,
0x8004F50C). Holding it changes nothing: the System Administrator role already bypasses field security.
Reads. A secured column the caller cannot read on a record is handled the same way wherever it was asked for (a
column set, AllColumns, $select, $expand, a FetchXML attribute or link-entity):
- the SDK and SOAP return the column's key with a null value, so a protected value looks like an empty one;
- the Web API leaves the property out of the JSON;
- the value is not read from SQL, unless a field share may grant it on some of the rows (then it is read and blanked per row);
- a filter, order, aggregate,
groupbyor link join on it is refused before any SQL runs, with 4030x8004F504, in FetchXML (nested filters too),QueryExpression,QueryByAttribute,$filter,$orderbyand the options of an$expand. It is an error, never an empty result, also when a field share would grant the column on some rows.count(*)is allowed; RetrieveDuplicatesis refused with the same 403 when a published duplicate detection rule it uses compares such a column (the rule's conditions are filters on the matches; see Duplicate detection);- downloading a secured File or Image column needs read (403
0x8004F504); the companion columns follow the column (see above); - the audit messages leave out the old and new values of such columns, and
RetrieveAttributeChangeHistoryon one is a 4030x8004F504. Theaudittable keeps the full values for administrators.
Writes. Checked before the pipeline, on the SDK, SOAP and Web API paths:
- Create: a secured column sent with a value needs create permission (403
0x8004F502); sent as null it is accepted, since it writes nothing. - Update: a secured column present in the request needs update permission, or a
field share with update access on that record (403
0x8004F507), even when the value is unchanged or null. - Starting an upload to, or deleting, a secured File or Image column needs update permission (403
0x8004F507). - A 403
0x8004F507raised by a request a plug-in makes through its organization service reaches the original caller as the 400 that wraps every plug-in failure, with the plug-in's message.
Field sharing. A principalobjectattributeaccess row gives one user or team read and/or update on one secured
column (attributeid, the column's MetadataId) of one record (objectid), with readaccess and updateaccess.
Shares are created, changed and deleted with plain Create, Update and Delete (entity set
principalobjectattributeaccessset). The server checks, in this order:
- the column is secured (400
0x8004F50E); - at least one access is granted (400
0x8004F509on create,0x8004F50Aon update; delete the share instead); - the same column of the same record is not already shared with that principal (400
0x8004F50B; a unique index enforces it under concurrency); - the caller can read the record (the usual record-level 403);
- the caller can update the column on that record and, when the share grants read, also read it there (403
0x8004F503): nobody gives more than they have. Deleting a share needs the same rights as granting it.
Only readaccess and updateaccess change on Update. A share given to a user or team that does not exist is a 404. A
share on a team applies to every member. The shares of a record
are deleted with the record, those of a user or team with the principal. The Basic User role holds create, read, write
and delete on principalobjectattributeaccess (Global) in organizations created by this version.
Error codes (values from the Microsoft documentation, messages in our own words):
| Code | HTTP | When |
|---|---|---|
0x8004F501 (-2147158783) |
400 | IsSecured on a column that cannot be secured |
0x8004F502 (-2147158782) |
403 | Create sets a secured column without create permission |
0x8004F503 (-2147158781) |
403 | a field share grants more than the sharer holds |
0x8004F504 (-2147158780) |
403 | reading, filtering, ordering, aggregating or joining on a secured column without read permission |
0x8004F507 (-2147158777) |
403 | Update (or a file upload) of a secured column without update permission |
0x8004F508 (-2147158776) |
403 | IsSecured turned off without read, create and update permission on the column (code from memory, not verified) |
0x8004F509 (-2147158775) |
400 | a new field share grants neither read nor update |
0x8004F50A (-2147158774) |
400 | an Update leaves a field share with neither read nor update |
0x8004F50B (-2147158773) |
400 | the column of that record is already shared with that principal |
0x8004F50C (-2147158772) |
403 | the System Administrator profile is changed or deleted |
0x8004F50E (-2147158770) |
400 | a permission or field share on a column that is not secured |
0x80040217 (-2147220969) |
404 | a permission on a table or column that does not exist |
Solutions. IsSecured travels with the column. Profiles are solution components of type 70: they are exported and
imported in customizations.xml under FieldSecurityProfiles. Each FieldSecurityProfile has the attributes
name, description and fieldsecurityprofileid and holds its FieldPermission elements in a FieldPermissions
element. A permission's values are child elements, in this order: EntityName, AttributeName, CanRead,
CanUpdate, CanCreate, CanReadUnmasked; CanRead, CanUpdate and CanCreate are 0 or 4, and CanReadUnmasked is
imported as 0. This shape is confirmed against a real unmanaged Dataverse export
(tests/fixtures/solutions/RealExports/OpenProject365_1_0_1_1.zip). Only the group names of a profile in the import log
(fieldSecurityProfiles, fieldSecurityProfile) are not yet compared with Dataverse (checklist). On import:
a solution that carries profiles needs the create and write privileges on
fieldsecurityprofileandfieldpermission(in practice the System Administrator role): otherwise the import fails with 403 before anything is written;a
FieldPermissionplaced directly under its profile, without theFieldPermissionselement, is not imported and the import log says so;the profile's permissions are replaced by those in the file;
a permission on a column that is not secured in the organization is skipped with a warning, as is a second permission on the same column (the first one wins);
canreadunmaskedother than 0 is imported as 0 with a warning (no masking, see below);the System Administrator profile is never imported (warning), exported or deleted by a cascade solution delete, and
AddSolutionComponentrefuses it (400);users and teams holding a profile never travel.
Existing organizations. Before this version, IsSecured was stored when a solution set it but nothing enforced it,
and profiles were not imported. From this version those columns are enforced: users other than System Administrators no
longer read them, and an Update that names one is refused, until a profile lets them in. Re-import the solution (its
profiles now travel with it) or create profiles with xrmd fieldsecurity or the web admin. To see which columns are
secured: the web admin Field security matrix lists them, each column of the Tables page shows its Secured switch,
GET /api/data/v9.2/EntityDefinitions(LogicalName='<table>')/Attributes returns IsSecured per column, and the MCP
describe_table tool marks them with "secured": true.
Tools. xrmd fieldsecurity (profiles, profile create|delete, permission set|remove, assign|unassign --user |--team, secure|unsecure; see CLI xrmd) and the web admin Field security page and Secured switches (see
Web admin). No new MCP tool and no Admin API endpoint.
Known limits
Each of these is either refused with an error or stated here; none is a silent difference.
- Hierarchy security does not widen column access. A manager who reads a report's record through hierarchy security sees a secured column only through the manager's own profiles and field shares, never the report's. The record Read check of the field-share rules includes the hierarchy reach, so a manager with the column rights can share that column of a report's record.
- Masking is not supported.
canreadunmaskedother than 0 is a 400 on a direct write (on solution import it becomes 0 with a warning), and no masking rules are seeded. Values are never masked, so asking for unmasked data (UnMaskedDatatrue on Retrieve or RetrieveMultiple through the SDK, SOAP or a plug-in's organization service, or as a Web API query option, including on a single column's URL) is a 400; false, or leaving it out, is accepted. - The web admin record form lets a user edit a secured column that the user can read but not update; saving then
fails with 403
0x8004F507. The editor is not disabled ahead of time. - Existing organizations: the Basic User role of an organization created before this version does not get the
principalobjectattributeaccessprivileges, so its users get a 403 when they share a column. Add the four privileges to the role (AddPrivilegesRole) to allow field sharing. - Distinct queries return null for a secured column the caller cannot read through a profile, even on a record where a field share grants it.
- Field shares do not reach
fileattachmentrows. The row of a secured File or Image column is hidden from a caller without read on the column through a profile, even on a record where a field share grants that column; the file itself can still be downloaded there through the column. This errs on the side of hiding. - Plug-in images are built for the plug-in's registered user and are not filtered: a pre- or post-image can carry a secured value. A column a pre-operation plug-in adds to the Target is not checked again.
Upsertis not supported by xrmverse; when it is added it must apply the Create and Update checks above.- Not implemented: the
applicationuserprofilerelationship;RetrieveUserPrivileges,RetrievePrincipalAccessandRetrieveAttributePrivileges(planned with the web UI work); secured columns as sources of calculated and rollup columns (these do not exist yet). The SQL endpoint applies field security: a secured column the user cannot read isNULL, and filtering on it is refused. - Not verified against a real Dataverse: the object type codes of the five tables (1200, 1201, 1202, 1203 and 44),
the System Administrator profile id, which columns are securable, the Basic User privileges on field shares, the code
0x8004F508, whether a System Customizer may import a solution that carries profiles, and the group names of profiles in the import log.
The manual acceptance checklist is
docs/superpowers/acceptance/field-security-checklist.md.