From power-pages
Audits table permissions on Power Pages sites against site code and Dataverse metadata. Generates HTML report with severity-grouped findings (critical, warning, info, pass) and fix suggestions.
npx claudepluginhub microsoft/power-platform-skills --plugin power-pagesThis skill is limited to using the following tools:
Audit existing table permissions on a Power Pages code site. Analyze permissions against the site code and Dataverse metadata, then generate a visual HTML audit report with findings, reasoning, and suggested fixes.
Integrates Power Pages Web API into frontend code sites for Dataverse tables, implementing API clients, CRUD operations, permissions setup, and deployment.
Audits Supabase Row Level Security policies across all tables by scanning migrations, database types, client code, and storage buckets. Flags missing RLS and intentional skips.
Enforces Supabase project governance: shared RLS policy templates, naming conventions, CI migration reviews, cost alerts, security audits for common misconfigurations.
Share bugs, ideas, or general feedback.
Audit existing table permissions on a Power Pages code site. Analyze permissions against the site code and Dataverse metadata, then generate a visual HTML audit report with findings, reasoning, and suggested fixes.
.powerpages-site folder and table permissions existImportant: Do NOT ask the user questions during analysis. Autonomously gather all data, then present findings.
At the start of Step 1, create all tasks upfront using TaskCreate. Mark each task in_progress when starting and completed when done.
| Task subject | activeForm | Description |
|---|---|---|
| Verify site deployment | Verifying site deployment | Check .powerpages-site folder and table permissions exist |
| Gather configuration | Gathering configuration | Read web roles, table permissions, and site code |
| Run local schema validation | Validating local permissions schema | Run shared validator against existing table permission and site setting YAML |
| Discover relationships | Discovering relationships | Query Dataverse for lookup columns and relationships |
| Run audit checks | Running audit checks | Create per-table tasks and run checklist (A–K) for each table, then cross-validate |
| Generate audit report | Generating audit report | Create HTML report and display in browser |
| Present findings | Presenting findings | Summarize results, record usage, and offer to fix issues |
Note: The "Run audit checks" phase creates additional per-table tasks dynamically in Step 4.2. These per-table tasks track the systematic A–K checklist for each table independently.
Use Glob to find:
**/powerpages.config.json — identifies the project root**/.powerpages-site/table-permissions/*.tablepermission.yml — existing permissionsIf no .powerpages-site folder exists, stop and tell the user to deploy first using /deploy-site.
If no table permissions exist, note this as a critical finding (the site may have no data access configured) and continue the audit — there may still be code references that need permissions.
Read all files matching **/.powerpages-site/web-roles/*.yml. Extract id, name, anonymoususersrole, authenticatedusersrole from each.
Read all files matching **/.powerpages-site/table-permissions/*.tablepermission.yml. For each permission, extract:
entityname (permission name)entitylogicalname (table)scope (numeric code)read, create, write, delete, append, appendto (boolean flags)adx_entitypermission_webrole (array of web role UUIDs)contactrelationship, accountrelationship (if Contact/Account scope)parententitypermission, parentrelationship (if parent scope)Search the site source code for:
/_api/)@odata.bind)uploadFileColumn, uploadFile, upload*Photo, upload*Image)$expand usage ($expand, buildExpandClause, ExpandOption)Also check for .datamodel-manifest.json in the project root for the authoritative table list.
Build a map of: which tables are referenced in code, which CRUD operations are performed on each, which lookup relationships are used, and which related tables are fetched via $expand (these need read permissions too).
Run the shared validator against the existing site:
$schemaValidation = node "${CLAUDE_PLUGIN_ROOT}/scripts/validate-permissions-schema.js" --projectRoot "<PROJECT_ROOT>"
Parse the JSON output and carry the findings into the audit. Treat:
error findings as criticalwarning findings as warninginfo findings as infoThese findings should be included in the final audit report even if the later code/Dataverse analysis also finds additional issues.
After Step 3.1 determines $envUrl, if this audit is running locally with Dataverse access available, rerun the shared validator with live relationship verification enabled and merge any additional findings:
$schemaValidation = node "${CLAUDE_PLUGIN_ROOT}/scripts/validate-permissions-schema.js" --projectRoot "<PROJECT_ROOT>" --validate-dataverse-relationships --envUrl "$envUrl"
Use this Dataverse-backed relationship validation only for local runs. Do not require it in CI or other offline contexts.
Use deterministic Node.js scripts for all Dataverse API calls. These scripts handle auth token acquisition, HTTP requests, and JSON parsing consistently.
pac env who
Extract the Environment URL (e.g., https://org12345.crm.dynamics.com). Store as $envUrl.
For each table that has permissions with create or write enabled, use the lookup query script:
$lookups = node "${CLAUDE_PLUGIN_ROOT}/skills/audit-permissions/scripts/query-table-lookups.js" --envUrl "$envUrl" --table "<table_logical_name>"
The script returns a JSON array of { logicalName, targets } for each lookup column.
After querying all tables with create or write permissions, build two maps from the combined results:
appendto on the source table.targets array, record which source table(s) reference it. Used in Section H to check append on the target table.Example: querying order_item returns [{ logicalName: "cr4fc_orderid", targets: ["cr4fc_order"] }]
order_item → [{ column: "cr4fc_orderid", targets: ["cr4fc_order"] }]cr4fc_order → [{ sourceTable: "order_item", column: "cr4fc_orderid" }]Both maps are used in Sections H and H2:
appendto: true — it links TO other records (checked via the source map)targets needs append: true — other records link TO it (checked via the reverse target map)For tables with parent-scope permissions, verify the relationship names using the relationship query script:
$rels = node "${CLAUDE_PLUGIN_ROOT}/skills/audit-permissions/scripts/query-table-relationships.js" --envUrl "$envUrl" --table "<parent_table>"
The script returns a JSON array of { schemaName, referencedEntity, referencingEntity, referencingAttribute }. Use schemaName to validate the parentrelationship value in parent-scope permissions.
If any script exits with code 1, skip the API-dependent checks and note which checks were skipped in the report. Do NOT stop the entire audit for auth errors. Use the data model manifest and code analysis as fallback.
Use per-table task tracking to systematically run every audit check. Each check produces a finding with severity, title, reasoning, and a suggested fix. Findings can be critical, warning, info, or pass.
First, build a combined list of all tables to audit from two sources:
The union of these two sets is the complete audit scope. Each table will be audited from both directions: "does the code need a permission that doesn't exist?" and "does the permission match what the code actually does?"
For each table in the audit inventory, create a task:
TaskCreate:
subject: "Audit <table_logical_name>"
activeForm: "Auditing <table_display_name> permissions"
description: "Run all audit checks for <table_logical_name>"
Also create a summary task:
TaskCreate:
subject: "Compile audit findings"
activeForm: "Compiling audit findings"
description: "Combine all per-table findings into the final report"
Use TaskList at any point to review progress and see which tables still need auditing.
For each table, mark its task in_progress and run through the following checks in order. For every finding, note the specific evidence (file path, permission name, code pattern) that supports it. Skip checks that don't apply to this table.
A. Permission Existence
Does this table have a table permission?
criticalMissing permission for <table>infoUnused permission for <table>pass, proceed to remaining checksB. Web Role Association
Does the permission have web role(s) assigned?
adx_entitypermission_webrole — if empty or missing → finding:
warningPermission <name> has no web role associationpassC. Scope Appropriateness
Is the scope the least-privileged option that fits?
getCurrentContactId, _contactid_value, contactid) and account-scoped filters (_accountid_value, parentcustomerid)756150000) with write or delete enabled → finding:
warningGlobal scope with write/delete on <table>read → pass (acceptable for public reference data)warningScope could be narrower for <table>passD. Read Permission
Is read correctly set?
/_api/<entity_set>, list/get functions (list<TableName>, get<TableName>)read: false → finding:
criticalMissing read permission for <table>read: trueread: true and code reads → passE. Create Permission
Is create correctly set?
method: 'POST'), create functions (create<TableName>)create: false → finding:
criticalMissing create permission for <table>create: truecreate: true but no create patterns in code → finding:
infoCreate enabled but not used for <table>create if not neededpassF. Write Permission
Is write correctly set?
method: 'PATCH'), update functions (update<TableName>), file upload patterns (uploadFileColumn, uploadFile, upload*Photo, upload*Image, upload*File)write: false → finding:
criticalMissing write permission for <table>write: truewrite: false → finding:
warningFile upload detected but write is disabled on <table>write: truewrite: true but read: false → finding:
warningWrite enabled without read on <table>read: truewrite: true but no write patterns in code → finding:
infoWrite enabled but not used for <table>write if not neededpassG. Delete Permission
Is delete correctly set?
method: 'DELETE'), delete functions (delete<TableName>)delete: false → finding:
criticalMissing delete permission for <table>delete: truedelete: true but no delete patterns in code → finding:
infoDelete enabled but not used for <table>delete if not neededpassH. Append (target table check)
Does this table need append: true? Append is required on the target table — the table that other records link TO via lookup columns.
create or write permissions?@odata.bind references to this table's entity set (e.g., /<entity_set>()append: false → finding:
criticalMissing append on <table><source_table> has lookup column <column> targeting this table and sets it during create/write. The target table needs append permission so records can be linked to it. Users will see "You don't have permission to associate or disassociate"append: trueappend: true and justified → passappend: true but this table does NOT appear in the reverse target map and no code references it as a lookup target → finding:
infoAppend enabled but not needed on <table>append if not neededH2. AppendTo (source table check)
Does this table need appendto: true? AppendTo is required on the source table — the table that has lookup columns linking TO other records.
@odata.bind patterns in create/update calls for this tablecreate or write enabled, but appendto: false → finding:
criticalMissing appendto on <table><column> targeting <target_table> during create/write, which requires appendto permission. Users will see "You don't have permission to associate or disassociate"appendto: trueappendto: true and justified → passI. Parent Chain Integrity
If the permission has Parent scope (756150003):
parententitypermission references a valid permission ID that existsparentrelationship is a valid Dataverse relationship (if API available, using Step 3.3 results)criticalBroken parent chain for <permission>passJ. $expand Related Table Coverage
Is this table fetched via $expand on another table's query?
$expand analysis from Step 2.3 (search site source code for $expand, buildExpandClause, ExpandOption)read: true for the same web role → finding:
criticalMissing read permission for expanded table <table>$expand on <parent_table> in <service_file>, but has no read permission. Power Pages enforces table permissions on every entity in the query.read: true for the same web role. For collection-valued expansions (one-to-many), use Parent scope with the relationship name. For single-valued expansions (lookups to reference data), use Global scope with read-only access.passK. Record Findings & Complete
After all checks, mark the table's task as completed via TaskUpdate.
After all per-table audits are complete, run these cross-table checks:
appendto: true, (b) every target table in the reverse map has append: true, (c) no table has appendto: true without lookup columns in the source map, (d) no table has append: true without being in the reverse target map$expand usage, verify the expanded table has read: trueUse TaskList to review all completed audits, then mark the "Compile audit findings" task as in_progress and proceed to Step 5.
powerpages.config.json exists): write to <PROJECT_ROOT>/docs/permissions-audit.htmlDo NOT generate HTML manually or read/modify the template yourself. Use the render-audit-report.js script which mechanically reads the template and replaces placeholder tokens with your data.
Write a temporary JSON data file (e.g., <OUTPUT_DIR>/audit-data.json) with these keys:
{
"SITE_NAME": "The site name (from powerpages.config.json or folder name)",
"AUDIT_DESC": "Security audit of table permissions for Contoso Portal",
"SUMMARY": "2-3 sentence summary of the audit results",
"FINDINGS_DATA": [/* array of finding objects */],
"INVENTORY_DATA": [/* array of current permission objects */]
}
FINDINGS_DATA format:
{
"id": "f1",
"severity": "critical",
"title": "Missing permission for cra5b_product",
"table": "cra5b_product",
"scope": null,
"permission": null,
"reasoning": "The table cra5b_product is referenced in src/services/productService.ts with GET requests to /_api/cra5b_products, but no table permission exists for this table.",
"fix": "Create a table permission with Global scope and read-only access for the Anonymous Users role.",
"details": "Referenced in: src/services/productService.ts (line 23), src/components/ProductList.tsx (line 45)"
}
severity: One of critical, warning, info, passtable: The table logical name this finding relates to (or null for general findings)scope: The current scope if applicable (numeric code or friendly name), or nullpermission: The permission name if this finding is about an existing permission, or nullreasoning: Detailed explanation of why this is an issue — reference specific code files, line patterns, or Dataverse metadatafix: Actionable suggestion for how to resolve the issue (or null for pass findings)details: Additional context like file references, column names, or relationship detailsINVENTORY_DATA format:
{
"name": "Product - Anonymous Read",
"table": "cra5b_product",
"scope": "Global",
"roles": ["Anonymous Users"],
"read": true,
"create": false,
"write": false,
"delete": false,
"append": true,
"appendto": false
}
Run the render script (it creates the output directory if needed):
node "${CLAUDE_PLUGIN_ROOT}/scripts/render-audit-report.js" --output "<OUTPUT_PATH>" --data "<DATA_JSON_PATH>"
Delete the temporary data JSON file after the script succeeds.
Open the generated HTML file in the user's default browser.
Reference:
${CLAUDE_PLUGIN_ROOT}/references/skill-tracking-reference.md
Follow the skill tracking instructions in the reference to record this skill's usage. Use --skillName "AuditPermissions".
Present a summary to the user:
AskUserQuestion: "Would you like me to fix any of these issues? I can create or update table permissions to resolve the critical and warning findings."If the user wants fixes applied:
$expand read coverage — any finding that would result in a 403 Forbidden response from the Power Pages Web API): Spawn the table-permissions-architect agent using the Agent tool. Pass it a prompt that includes the specific tables, the required CRUD flags, scope recommendations, and relationship details from the audit findings. The agent will analyze the site, propose a permissions plan, and create the correct table permission YAML files after user approval. Example prompt: "Create table permissions for the following tables based on audit findings: <table1> needs Global scope with read:true; <table2> needs Parent scope under <parent_table> with read:true, create:true, append:true; <table3> needs appendto:true for lookups from <source_table>. The site project root is <PROJECT_ROOT>."/integrate-webapi so the Web API settings architect can address site-setting-level issues.query-table-lookups.js, query-table-relationships.js) for Dataverse API queries — never use inline PowerShell Invoke-RestMethod calls.getAuthToken().