Table-level security controls who can access records, but real production environments often need finer control — down to individual columns and specific records. This lesson teaches you how to implement Field Security Profiles, configure column-level access, and use record sharing to grant targeted exceptions without restructuring your entire security model.

Imagine you're building a model-driven app for your company's HR department. The app manages employee records, and dozens of people across the organization need to use it — recruiters who need to see job titles and departments, managers who need to view performance notes, payroll staff who need access to salary figures, and IT who just need to look up email addresses and cost centers. The problem is obvious: you can't give everyone the same view of the data. A recruiter has no business seeing an employee's base salary. A manager reviewing performance shouldn't see bank account details. And the last thing you want is a misconfigured security role accidentally exposing compensation data to the whole company.
Dataverse's table-level and row-level security — handled through security roles and business units — gets you most of the way there. If you've worked through Dataverse Security: Business Units, Security Roles, and Teams, you know how to control which tables and rows different users can access. But that security model operates at the row level: a user either can or cannot read a record. What it doesn't solve, by default, is the case where a user needs to see most of a record but not all of it. That's where Column-Level Security (also called Field-Level Security) enters the picture. And alongside that, Dataverse's sharing mechanism — the ability to grant one specific user or team access to one specific record — fills the gap between rigid role-based access and the real-world messiness of who actually needs to see what.
By the end of this lesson, you'll be able to design and implement a production-grade security model that controls access at the individual column level, configure field security profiles and assign them to users and teams, use record sharing to grant targeted access without changing security roles, understand the performance and governance tradeoffs of each approach, and debug common configuration mistakes before they cause problems in production.
What you'll learn:
PrincipalObjectAccess table and sharing from the UI and APIThis lesson assumes you're comfortable with the core Dataverse security model. You should understand security roles, business units, and the difference between organization-scoped, business-unit-scoped, and user-owned access before continuing. If you need a refresher, read Dataverse Security: Business Units, Security Roles, and Teams first. You should also have a working familiarity with Dataverse tables and columns — the article on Dataverse Fundamentals: Tables, Columns, and Rows Explained for Power Apps Makers covers that ground.
Before touching any settings, it's worth understanding the model conceptually — because the way Dataverse implements column security is different from what most people expect.
In most database systems, column-level security is a property of the table or the column itself: you grant SELECT on a specific column to a specific role. Dataverse takes a different approach. By default, every column in Dataverse has no column-level security whatsoever. Access to a column is inherited entirely from the row-level access: if you can read the row, you can read every column in it.
To lock down a specific column, you must do two things:
Once you enable field-level security on a column, anyone who doesn't have an applicable Field Security Profile loses access to that column, regardless of what their security role says about the table. That's a critical point: enabling field security on a column is not additive. It's a hard restriction. Users who previously could read a column will be locked out the moment you enable field security on it, unless they're included in a profile that explicitly grants them Read permission.
Warning
When you enable field-level security on a column that already contains data, existing users will immediately lose visibility of that column's values — even the system administrator UI will show it as locked unless the admin has a Field Security Profile. Plan your rollout carefully, especially for columns that feed into views, forms, and business rules.
Let's use a concrete scenario throughout this lesson: a Compensation table with columns for Base Salary, Bonus Target, Equity Grant, Performance Rating, and Manager Notes. Everyone in HR can see the table and basic metadata, but only Payroll Analysts should see salary and bonus figures, and only Senior HR Business Partners should see performance ratings and manager notes.
To enable field-level security on the Base Salary column:
Base Salary and open it for editing.You'll need to repeat this for each column you want to protect: Bonus Target, Equity Grant, Performance Rating, and Manager Notes.
Note
Column-level security can only be enabled on custom columns and a subset of standard columns. You cannot enable it on the primary name column, system columns like Created On, or certain locked platform columns. If the toggle isn't available, that column isn't eligible for field security.
After enabling security, go look at your model-driven app form. Those columns will now show as empty for any user without a profile — they won't see an error, they'll just see blank values. This makes troubleshooting tricky if you don't know what to look for.
A Field Security Profile is a named container that specifies, for a set of secured columns, which combination of Create, Read, and Update operations are allowed. You then assign users or teams to that profile.
To create a profile:
Compensation: Payroll Full Access or Compensation: Read Only - Salary. You'll thank yourself when you have fifteen profiles to manage.Base Salary) and set the Read, Create, and Update toggles appropriately.For the Payroll Analyst profile, you'd add permissions for Base Salary and Bonus Target with Read, Create, and Update all set to Yes. For a Read-Only Auditor profile, you'd set only Read to Yes and leave Create and Update as No.
After defining field permissions, navigate to the Users or Teams tab on the profile to assign principals:
Tip
Assign Field Security Profiles to teams rather than individual users wherever possible. When you assign to a team, every member of that team inherits the profile, and membership management stays in Azure AD or the team configuration rather than requiring admin portal changes every time someone changes roles.
Here's where many practitioners get tripped up: field security and security roles are both required, and they operate independently. A user needs:
Compensation table at the appropriate scope (user-owned, business unit, or organization).These are additive requirements. Giving someone a Field Security Profile with Read access to Base Salary does nothing if their security role doesn't let them read the Compensation table at all. Conversely, a security role that grants full access to the Compensation table doesn't unlock secured columns — the Field Security Profile gate is always enforced independently.
Think of it as two separate locks on a safe. The security role is the combination lock on the door of the vault. The Field Security Profile is the combination lock on an inner compartment. You need both combinations.
There is one important exception: System Administrators bypass field-level security entirely. An account with the System Administrator security role can always read and write any column, even secured ones, without needing a Field Security Profile. This is by design, but it means your system admin accounts should be treated as high-privilege accounts — which they always should have been, but it's worth stating explicitly.
Key insight
If you're troubleshooting a user who can see a table but can't see certain column values, the first thing to check is whether those columns have field security enabled and whether that user (or any team they belong to) has an applicable Field Security Profile with Read = Yes.
Once you have field security in place across multiple tables, you'll need a way to audit who has access to what. The Power Platform admin center gives you a list of profiles, but reviewing them one at a time doesn't scale.
A more practical approach is to query the underlying tables directly using the Dataverse Web API or via a canvas app or Power Automate flow. The key tables are:
FieldSecurityProfile — stores the profiles themselvesFieldPermission — stores the column-level permissions within each profileSystemUserProfiles and TeamProfiles — the many-to-many tables linking users and teams to profilesYou can use the following FetchXML to pull a summary of all field permissions across all profiles, which is useful for a security review:
<fetch>
<entity name="fieldpermission">
<attribute name="fieldpermissionid" />
<attribute name="entityname" />
<attribute name="attributelogicalname" />
<attribute name="canread" />
<attribute name="cancreate" />
<attribute name="canupdate" />
<link-entity name="fieldsecurityprofile" from="fieldsecurityprofileid" to="fieldsecurityprofileid" alias="profile">
<attribute name="name" />
</link-entity>
</entity>
</fetch>
Run this through the Web API at https://yourorg.crm.dynamics.com/api/data/v9.2/fieldpermissions?fetchXml=... (URL-encoded) or paste it into Advanced Find (legacy) in your model-driven app. The output gives you a table of every secured column, every profile, and what level of access that profile grants — exactly the kind of artifact you'd want for a compliance review.
Field security controls column access uniformly across all records in a table. Record sharing is a completely different mechanism that targets a specific record and grants access to a specific user or team — without modifying security roles or field security profiles.
This matters in scenarios like:
Dataverse implements sharing through a system table called PrincipalObjectAccess (POA). When you share a record with a user or team, Dataverse writes a row into this table that says: "Principal X has [Read/Write/Delete/Append/AppendTo/Assign/Share] access on Record Y." The security engine consults this table at query time to determine effective access.
Note
Sharing only applies to user-owned and team-owned tables — that is, tables where the ownership type is "User or Team." You cannot share rows from organization-owned tables (like most system metadata tables) because every authenticated user can already read those by design.
To share a record in a model-driven app:
The user you've shared with will now be able to access that specific record even if their security role wouldn't normally give them access to records they don't own.
The ability to share a record is itself a privilege controlled by the security role. The Share privilege on a table must be enabled in the user's security role before they can share records from that table. A user without the Share privilege on the Opportunity table cannot share opportunity records, even if they own them.
In a production system, you often need to share records automatically — for example, when a project is created, share it with all members of the assigned project team. You can do this with a Power Automate flow using the Perform an unbound action step with the GrantAccess Dataverse action.
Here's what that action call looks like in terms of the parameters you need to supply. In Power Automate, after adding a Perform an unbound action step connected to your Dataverse environment:
GrantAccess{
"@odata.type": "Microsoft.Dynamics.CRM.mshr_compensationjobrecord",
"mshr_compensationjobrecordid": "<record-guid-here>"
}
{
"Principal": {
"@odata.type": "Microsoft.Dynamics.CRM.systemuser",
"systemuserid": "<user-guid-here>"
},
"AccessMask": "ReadAccess, WriteAccess"
}
The AccessMask value is a comma-separated string of access rights. Available values are: ReadAccess, WriteAccess, DeleteAccess, AppendAccess, AppendToAccess, AssignAccess, ShareAccess.
Tip
Use RevokeAccess as the action name — same parameters — to remove sharing when it's no longer needed. In long-lived environments, accumulated sharing entries can become a maintenance and performance problem if they're never cleaned up.
This is the part that doesn't appear prominently in documentation but will absolutely matter at scale. The PrincipalObjectAccess table can grow very large in environments where sharing is used extensively. Every shared record creates at least one row in this table. In organizations with thousands of users and millions of records, POA can end up with tens of millions of rows.
The consequence is that queries in Dataverse that need to compute effective access — which is almost every query for user-owned records — must join against POA. When POA is large, those joins get slower. You'll see this manifest as slower view load times and slower API responses, even on paginated queries that return small result sets.
Practical guidance for managing this:
RevokeAccess when the step completes.SELECT COUNT(*) FROM PrincipalObjectAccessReadSnapshotView in a SQL-based environment, or track it via the Dataverse Web API: GET /api/data/v9.2/principalobjectaccessset?$select=objectid&$top=1 won't give you a count, but your admin tooling should surface table sizes.Warning
In environments that have been running for several years with heavy sharing usage, POA can have hundreds of millions of rows. If you're joining a project to implement field security or sharing on an existing mature environment, audit the current POA size before making changes. Adding field security doesn't affect POA, but if you're also redesigning sharing patterns, you may want to clean up POA as part of the project.
Dataverse offers two kinds of teams that behave differently for sharing purposes, and choosing the right one significantly affects your maintenance overhead.
Owner teams own records outright. Records can be assigned to an owner team, and every member of that team inherits access to all records owned by the team. This is the right choice when a group of people collectively owns and manages a set of records — for example, a "West Region Sales" team that collectively manages all West Region opportunities.
Access teams are a specialized sharing mechanism. An access team is a lightweight, record-specific team. Rather than assigning records to the team (as with owner teams), you add users to the access team for a specific record, and those users get a specific access mask on that record. Access team membership is per-record — the same user can be in different access teams on different records with different access rights.
Access teams are created via Access Team Templates, which you define on the table. To create an Access Team Template:
Access teams are ideal for collaborative review scenarios: a project that needs to be reviewed by a cross-functional panel, where each reviewer needs temporary write access to a specific record without getting access to every record in the table. When the review ends, you remove them from the access team on that record.
Access team membership is stored differently from POA — it's more efficient for per-record membership management — but it still has scale implications. Keep access teams bounded in size and actively manage membership lifecycle.
Let's return to our HR scenario and design the complete security model. Here's our requirements matrix:
| User Role | Base Salary | Bonus Target | Equity Grant | Performance Rating | Manager Notes |
|---|---|---|---|---|---|
| HR Generalist | No | No | No | Read | No |
| Payroll Analyst | Read/Write | Read/Write | Read | No | No |
| Senior HRBP | Read | Read | No | Read/Write | Read/Write |
| HR Director | Read/Write | Read/Write | Read/Write | Read/Write | Read/Write |
| Recruiter | No | No | No | No | No |
Step 1: Enable field security on Base Salary, Bonus Target, Equity Grant, Performance Rating, and Manager Notes.
Step 2: Create Field Security Profiles:
Compensation: Payroll Analyst — Base Salary (R/W), Bonus Target (R/W), Equity Grant (Read)Compensation: Senior HRBP — Base Salary (Read), Bonus Target (Read), Performance Rating (R/W), Manager Notes (R/W)Compensation: HR Director — All columns (R/W)Compensation: HR Generalist — Performance Rating (Read only)Notice that Recruiter doesn't need a Field Security Profile at all — they have no access to any of the sensitive columns. Their security role should still give them Read access to the Compensation table itself (so they can see non-sensitive metadata like job grade and department), but they don't get a profile.
Step 3: Assign profiles to teams. Create Azure AD security groups for each role, create Dataverse teams backed by those groups, and assign the appropriate Field Security Profile to each team.
Step 4: Configure security roles. Each role should have appropriate table-level access to Compensation. For most HR roles, this is probably business-unit scope Read (and Write for the roles that submit compensation changes). This is defined on the security role, independent of the field security configuration.
Step 5: Design your forms thoughtfully. In your model-driven app forms, you can still place all the sensitive columns on the form. When a user without an appropriate Field Security Profile views the form, those fields render as locked/blank automatically — you don't need to hide them with JavaScript or business rules. The platform enforces it. That said, for cleanliness, you might use separate tabs or sections for sensitive data, as discussed in Designing Model-Driven Forms: Sections, Tabs, Subgrids, and Quick View Forms, so that users without access don't see a confusing wall of blank locked fields.
Key insight
Field-level security enforcement happens at the data layer, not the UI layer. Even if a Power Automate flow or a custom API call from a canvas app reads the Compensation table, the secured column values will be null for any caller whose associated user account lacks a Field Security Profile granting Read access. This is fundamentally different from hiding a field on a form — it's real security.
Something worth knowing: field-level security applies to views and reports too. If Base Salary has field security enabled and a user opens a view that includes Base Salary as a column, they'll see blank values in that column (not an error, just blank) unless they have a Field Security Profile granting Read access.
This creates a UX design decision. You can:
Build separate views for different audiences — a Payroll view that includes salary columns, and a General HR view that doesn't. Users will only be able to effectively use the salary-inclusive view if they have the right profile, but the view is still technically accessible to anyone who has Read access to the table.
Let the single view show blanks for unauthorized users. This is simpler to maintain but can be confusing.
Option 1 is usually better for end users. Use view filtering and the techniques in Creating and Customizing Views in Model-Driven Apps: Filters, Sorting, and Editable Grids to create role-appropriate default views, and configure the app to show different views to different user segments.
For this exercise, you'll implement a full column-level security setup on a realistic table.
Scenario: You're building a model-driven app for a consulting firm. The Project table has columns including Project Name, Client Name, Agreed Fee, Margin Percentage, Consultant Notes, and Client Satisfaction Score. Consultants should see project name and client name, but not financial data. Project managers should see everything except margin. Finance staff should see all columns.
Exercise steps:
In a development environment, create a Project table with the columns listed above (use Decimal or Currency types for financial columns).
Enable field-level security on Agreed Fee, Margin Percentage, and Client Satisfaction Score.
Create three Field Security Profiles:
Project: Finance Full Access — Read/Write on all three secured columnsProject: Project Manager Access — Read/Write on Agreed Fee and Client Satisfaction Score, no access to Margin PercentageProject: Consultant Read Only — Read only on Client Satisfaction Score, no access to Agreed Fee or Margin PercentageCreate three test user accounts (or use existing ones if available) and assign them individually to the profiles to test (in production you'd use teams, but for testing individual assignment is faster to validate).
Build a simple model-driven app with a Project form that includes all columns. Log in as each test user and verify that the correct columns are visible and editable.
Now test record sharing: while logged in as the Finance user, share a specific project record with the Consultant user and grant Write access. Log in as the Consultant user and verify they can now edit that specific record (but only the columns their Field Security Profile permits — sharing does not override field security).
Revoke the sharing and confirm the Consultant user loses write access to that record.
The final step in step 6 is important: it illustrates that record sharing and field security are independent axes of control. Sharing a record grants row-level access; it does not grant column-level access. A user who receives a shared record with Write access can still only write to the columns their Field Security Profile permits.
Mistake 1: Enabling field security without creating any profiles
The moment you enable field security on a column, every user who doesn't have a Field Security Profile loses access to it. If you enable it on Base Salary before creating and assigning the Payroll Analyst profile, your payroll team will immediately see blank salary fields. Always create and assign profiles before enabling field security on the column, or do the whole thing in a maintenance window.
Mistake 2: Assigning profiles to users instead of teams
Individual user assignment works, but it creates an administrative nightmare. Every time someone joins or leaves a team, you have to manually update Field Security Profile assignments. Assign to AAD Group Teams and manage membership through Azure AD.
Mistake 3: Expecting sharing to override field security
As covered above, it doesn't. If a user's Field Security Profile doesn't grant Read on Margin Percentage, they won't see it even on records explicitly shared with them. This surprises people, but it's the correct and intended behavior — the point of field security is to be an unconditional column-level control.
Mistake 4: Not testing with real user accounts
System administrators bypass field security entirely. If you test your field security setup while logged in as a system admin, everything looks wide open and you'll think it's not working. Always test with non-admin test accounts that have only the security roles and profiles you intend to assign.
Mistake 5: Sharing individual records when business unit restructuring is the right answer
If you find yourself writing a flow that shares 500 records every time a user changes departments, the real solution is restructuring your business unit hierarchy or security role scoping so that the user's role inherently covers those records. Record sharing is for exceptions, not structural access patterns. See Dataverse Security: Business Units, Security Roles, and Teams for guidance on designing that structure correctly.
Mistake 6: Forgetting that Canvas Apps also respect field security
If you have a canvas app built on the same Dataverse tables — connected using the Dataverse connector — field security applies there too. A canvas app that calls Patch() to update a secured column for a user without Write permission in their Field Security Profile will receive an error. This is often discovered late in development. As you build canvas apps on Dataverse, make sure your test accounts include users with restricted field security profiles to catch this early. The Power Apps Security: Roles, Sharing, and Data Permissions article covers how this plays out in the canvas app context.
Debugging tip: Use the Dataverse Web API directly
When you're not sure whether a user has effective access to a column, the cleanest diagnostic is to call the Web API as that user:
GET https://yourorg.crm.dynamics.com/api/data/v9.2/mshr_compensations(<record-guid>)?$select=mshr_basesalary
If the user has Read access to the column via a Field Security Profile, the response includes the value. If not, the column is either omitted from the response or returned as null, depending on the Dataverse version. This tells you definitively whether the field security enforcement is working correctly for that user.
Column-Level Security and Record Sharing in Dataverse solve two related but distinct problems. Field security gives you unconditional, data-layer control over who can read or write individual columns — something that no amount of form design or UI tricks can replicate, because it's enforced by the platform before data ever reaches the application layer. Record sharing gives you surgical, row-level access grants for specific users or teams without restructuring your security roles — ideal for collaboration scenarios, temporary access needs, and exceptions to your standard access model.
The key design principles to carry forward:
From here, consider going deeper on the broader security architecture by revisiting Dataverse Security: Business Units, Security Roles, and Teams with this column-level layer in mind. If you're thinking about how your data model should be structured to make security configuration simpler, Designing a Dataverse Data Model: Relationships, Lookups, and Choice Columns will help you see how table design decisions upstream affect your security design options downstream. And if you're building canvas apps on top of these Dataverse tables, the patterns in Power Apps Security: Roles, Sharing, and Data Permissions show how security enforcement translates into the canvas app experience.
Security in production systems is never finished — it's a practice. Build audit habits, review Field Security Profile assignments regularly, and treat POA size as a metric worth monitoring alongside your query performance.