# Audit Permissions

> >-

- **Type:** Skill
- **Install:** `agentstack add skill-microsoft-power-platform-skills-audit-permissions`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [microsoft](https://agentstack.voostack.com/s/microsoft)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [microsoft](https://github.com/microsoft)
- **Source:** https://github.com/microsoft/power-platform-skills/tree/main/plugins/power-pages/skills/audit-permissions
- **Website:** https://aka.ms/ppskills

## Install

```sh
agentstack add skill-microsoft-power-platform-skills-audit-permissions
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.

# Audit Permissions

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.

## Workflow

1. **Verify Site Deployment** — Check that `.powerpages-site` folder and table permissions exist
2. **Gather Configuration** — Read all web roles, table permissions, and site code
3. **Run Local Schema Validation** — Use the shared validator to detect invalid permission/site-setting YAML before deeper analysis
4. **Analyze & Discover** — Query Dataverse for relationships and lookup columns using deterministic scripts
5. **Run Audit Checks** — Compare permissions against code usage and best practices
6. **Generate Report** — Create the HTML audit report and display in browser
7. **Present Findings & Track** — Summarize findings, record skill usage, and ask user if they want to fix issues

**Important:** Do NOT ask the user questions during analysis. Autonomously gather all data, then present findings.

## Task Tracking

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.

---

## Step 1: Verify Site Deployment

Use `Glob` to find:

- `**/powerpages.config.json` — identifies the project root
- `**/.powerpages-site/table-permissions/*.tablepermission.yml` — existing permissions

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

---

## Step 2: Gather Configuration

### 2.1 Read Web Roles

Read all files matching `**/.powerpages-site/web-roles/*.yml`. Extract `id`, `name`, `anonymoususersrole`, `authenticatedusersrole` from each.

### 2.2 Read Table Permissions

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)

### 2.3 Analyze Site Code

Search the site source code for:

- Web API calls (`/_api/`)
- Lookup bindings (`@odata.bind`)
- File uploads (`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).

### 2.4 Run Shared Schema Validator

Run the shared validator against the existing site:

```bash
node "${PLUGIN_ROOT}/scripts/validate-permissions-schema.js" --projectRoot ""
```

Parse the JSON output and carry the findings into the audit. Treat:

- `error` findings as **critical**
- `warning` findings as **warning**
- `info` findings as **info**

These 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 the environment URL, if this audit is running locally with Dataverse access available, rerun the shared validator with live relationship verification enabled and merge any additional findings:

```bash
node "${PLUGIN_ROOT}/scripts/validate-permissions-schema.js" --projectRoot "" --validate-dataverse-relationships --envUrl ""
```

Use this Dataverse-backed relationship validation only for local runs. Do **not** require it in CI or other offline contexts.

---

## Step 3: Analyze & Discover (Dataverse API)

Use deterministic Node.js scripts for all Dataverse API calls. These scripts handle auth token acquisition, HTTP requests, and JSON parsing consistently.

### 3.1 Get Environment URL

```bash
pac env who
```

Extract the `Environment URL` (e.g., `https://org12345.crm.dynamics.com`) and use it as `` in subsequent script calls.

### 3.2 Query Lookup Columns

For each table that has permissions with `create` or `write` enabled, use the lookup query script:

```bash
node "${PLUGIN_ROOT}/skills/audit-permissions/scripts/query-table-lookups.js" --envUrl "" --table ""
```

The script returns a JSON array of `{ logicalName, targets }` for each lookup column. Capture this output for the maps described below.

After querying **all** tables with create or write permissions, build two maps from the combined results:

1. **Source map** (table → lookup columns): For each queried table, record which lookup columns it has and their targets. Used in Section H2 to check `appendto` on the source table.
2. **Reverse target map** (target table → list of source tables): For each target table found in any lookup's `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"] }]`
- Source map: `order_item → [{ column: "cr4fc_orderid", targets: ["cr4fc_order"] }]`
- Reverse target map: `cr4fc_order → [{ sourceTable: "order_item", column: "cr4fc_orderid" }]`

Both maps are used in Sections H and H2:
- The **source table** (with the lookup) needs `appendto: true` — it links TO other records (checked via the source map)
- Each **target table** in `targets` needs `append: true` — other records link TO it (checked via the reverse target map)

### 3.3 Query Relationships

For tables with parent-scope permissions, verify the relationship names using the relationship query script:

```bash
node "${PLUGIN_ROOT}/skills/audit-permissions/scripts/query-table-relationships.js" --envUrl "" --table ""
```

The script returns a JSON array of `{ schemaName, referencedEntity, referencingEntity, referencingAttribute }`. Use `schemaName` to validate the `parentrelationship` value in parent-scope permissions.

### Error Handling

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.

---

## Step 4: Run Audit Checks

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

### 4.1 Build Audit Inventory

First, build a combined list of all tables to audit from two sources:

1. **Tables referenced in code** (from Step 2.3) — these may or may not have permissions
2. **Tables with existing permissions** (from Step 2.2) — these may or may not be referenced in code

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?"

### 4.2 Create Per-Table Audit Tasks

For each table in the audit inventory, create a task:

```
TaskCreate:
  subject: "Audit "
  activeForm: "Auditing  permissions"
  description: "Run all audit checks for "
```

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.

### 4.3 Per-Table Audit Checklist

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?

- If the table is referenced in code but has **no permission** → finding:
  - **Severity:** `critical`
  - **Title:** `Missing permission for `
  - **Reasoning:** Which code files reference this table and what operations they perform
  - **Fix:** Create a permission with the appropriate scope and CRUD flags
- If a permission exists but the table is **not referenced in code** → finding:
  - **Severity:** `info`
  - **Title:** `Unused permission for `
  - **Reasoning:** The table is not referenced in any source code — the permission may be unnecessary
  - **Fix:** Review whether this permission is still needed
- If both exist → `pass`, proceed to remaining checks

**B. Web Role Association**

Does the permission have web role(s) assigned?

- Check `adx_entitypermission_webrole` — if empty or missing → finding:
  - **Severity:** `warning`
  - **Title:** `Permission  has no web role association`
  - **Reasoning:** A permission without a web role has no effect — no users will receive this access
  - **Fix:** Associate with the appropriate web role
- If roles are assigned → `pass`

**C. Scope Appropriateness**

Is the scope the least-privileged option that fits?

- Search the service code for scope-relevant patterns: contact-scoped filters (`getCurrentContactId`, `_contactid_value`, `contactid`) and account-scoped filters (`_accountid_value`, `parentcustomerid`)
- If Global scope (`756150000`) with `write` or `delete` enabled → finding:
  - **Severity:** `warning`
  - **Title:** `Global scope with write/delete on `
  - **Reasoning:** Any user with this role can modify/delete any record in this table
  - **Fix:** Narrow to Contact or Account scope, or remove write/delete if not needed
- If Global scope with only `read` → `pass` (acceptable for public reference data)
- If code uses contact-scoped filters but permission uses Global → finding:
  - **Severity:** `warning`
  - **Title:** `Scope could be narrower for `
  - **Reasoning:** Code filters by current contact but permission grants Global access
  - **Fix:** Narrow to Contact scope
- Otherwise → `pass`

**D. Read Permission**

Is `read` correctly set?

- Search the service code for GET/list/get patterns for this table: API calls to `/_api/`, list/get functions (`list`, `get`)
- If code reads this table but `read: false` → finding:
  - **Severity:** `critical`
  - **Title:** `Missing read permission for `
  - **Reasoning:** Code reads from this table but permission does not grant read access
  - **Fix:** Enable `read: true`
- If `read: true` and code reads → `pass`

**E. Create Permission**

Is `create` correctly set?

- Search the service code for POST/create patterns: POST method usage (`method: 'POST'`), create functions (`create`)
- If code creates records but `create: false` → finding:
  - **Severity:** `critical`
  - **Title:** `Missing create permission for `
  - **Reasoning:** Code creates records in this table but permission does not grant create access
  - **Fix:** Enable `create: true`
- If `create: true` but no create patterns in code → finding:
  - **Severity:** `info`
  - **Title:** `Create enabled but not used for `
  - **Reasoning:** No create operations found in code — permission may be overly permissive
  - **Fix:** Consider disabling `create` if not needed
- If matched → `pass`

**F. Write Permission**

Is `write` correctly set?

- Search the service code for PATCH/update/upload patterns: PATCH method usage (`method: 'PATCH'`), update functions (`update`), file upload patterns (`uploadFileColumn`, `uploadFile`, `upload*Photo`, `upload*Image`, `upload*File`)
- If code updates records but `write: false` → finding:
  - **Severity:** `critical`
  - **Title:** `Missing write permission for `
  - **Reasoning:** Code updates records (or uploads files) in this table but permission does not grant write access
  - **Fix:** Enable `write: true`
- If file upload patterns found but `write: false` → finding:
  - **Severity:** `warning`
  - **Title:** `File upload detected but write is disabled on `
  - **Reasoning:** File uploads use PATCH which requires write permission
  - **Fix:** Enable `write: true`
- If `write: true` but `read: false` → finding:
  - **Severity:** `warning`
  - **Title:** `Write enabled without read on `
  - **Reasoning:** Users can modify records they cannot see, which is unusual and likely unintended
  - **Fix:** Enable `read: true`
- If `write: true` but no write patterns in code → finding:
  - **Severity:** `info`
  - **Title:** `Write enabled but not used for `
  - **Reasoning:** No update operations found in code — permission may be overly permissive
  - **Fix:** Consider disabling `write` if not needed
- If matched → `pass`

**G. Delete Permission**

Is `delete` correctly set?

- Search the service code for DELETE patterns: DELETE method usage (`method: 'DELETE'`), delete functions (`delete`)
- If code deletes records but `delete: false` → finding:
  - **Severity:** `critical`
  - **Title:** `Missing delete permission for `
  - **Reasoning:** Code deletes records in this table but permission does not grant delete access
  - **Fix:** Enable `delete: true`
- If `delete: true` but no delete patterns in code → finding:
  - **Severity:** `info`
  - **Title:** `Delete enabled but not used for `
  - **Reasoning:** No delete operations found in code — permission may be overly permissive
  - **Fix:** Consider disabling `delete` if not needed
- If matched → `pass`

**H. 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.

- Check the **reverse target map** from Step 3.2: is this table referenced as a lookup target by any other table that has `create` or `write` permissions?
- Also search the service code for `@odata.bind` references to this table's entity set (e.g., `/(`)
- If this table appears in the reverse target map (i.e., another table with create/write has a lookup to this table), but `append: false` → finding:
  - **Severity:** `critical`
  - **Title:** `Missing append on `
  - **Reasoning:** Table `` has lookup 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"
  - **Fix:** Enable `append: true`
- If `append: true` and justified → `pass`
- If `append: true` but this table does NOT appear in the reverse target map and no code references it as a lookup target → finding:
  - **Severity:** `info`
  - **Title:** `Append enabled but not n

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [microsoft](https://github.com/microsoft)
- **Source:** [microsoft/power-platform-skills](https://github.com/microsoft/power-platform-skills)
- **License:** MIT
- **Homepage:** https://aka.ms/ppskills

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-microsoft-power-platform-skills-audit-permissions
- Seller: https://agentstack.voostack.com/s/microsoft
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
