# Web Service Operation Access Audit

LLMS index: [llms.txt](/llms.txt)

---

<div id="pslens-context-panel" class="card border-info mb-4 d-none">
  <div class="card-header bg-light text-info py-2 fw-bold d-flex align-items-center border-bottom border-info-subtle">
    <i class="bi bi-info-circle-fill me-2"></i>
    <span>Tailored Operational Context</span>
  </div>
  <div class="card-body p-0">
    <ul class="list-group list-group-flush">
      <li id="row-db" class="list-group-item d-flex align-items-center justify-content-between py-2 d-none">
        <strong>Target Database:</strong>
        <span id="ctx-db" class="badge bg-secondary font-monospace">&mdash;</span>
      </li>
      <li id="row-type" class="list-group-item d-flex align-items-center justify-content-between py-2 d-none">
        <strong>Context Type:</strong>
        <span id="ctx-type" class="badge bg-light text-dark border font-monospace text-uppercase">&mdash;</span>
      </li>
      <li id="row-severity" class="list-group-item d-flex align-items-center justify-content-between py-2 d-none">
        <strong>Alert Severity:</strong>
        <span id="ctx-severity" class="badge">&mdash;</span>
      </li>
      <li id="row-time" class="list-group-item d-flex align-items-center justify-content-between py-2 d-none">
        <strong>Triggered Time:</strong>
        <span id="ctx-time" class="text-muted small">&mdash;</span>
      </li>
      <li id="row-details" class="list-group-item py-2 d-none">
        <strong id="label-details" class="d-block mb-1">Firing Context:</strong>
        <code id="ctx-details" class="d-block p-2 bg-light border rounded small" style="white-space: pre-wrap; word-break: break-all;">&mdash;</code>
      </li>
    </ul>
  </div>
</div>

<script>
  (function() {
    const params = new URLSearchParams(window.location.search);
    const metadata = params.get('metadata');
    if (!metadata) return;

    try {
      
      const base64 = metadata.replace(/-/g, '+').replace(/_/g, '/');
      const jsonStr = decodeURIComponent(escape(window.atob(base64)));
      const data = JSON.parse(jsonStr);

      if (data) {
        let hasData = false;

        if (data.db) {
          document.getElementById('ctx-db').textContent = data.db;
          document.getElementById('row-db').classList.remove('d-none');
          hasData = true;
        }

        if (data.type) {
          document.getElementById('ctx-type').textContent = data.type;
          document.getElementById('row-type').classList.remove('d-none');
          hasData = true;
        }

        if (data.severity) {
          const severityBadge = document.getElementById('ctx-severity');
          const severity = data.severity.toLowerCase();
          severityBadge.textContent = severity.toUpperCase();
          if (severity === 'critical') {
            severityBadge.className = 'badge bg-danger';
          } else if (severity === 'warning') {
            severityBadge.className = 'badge bg-warning text-dark';
          } else {
            severityBadge.className = 'badge bg-info';
          }
          document.getElementById('row-severity').classList.remove('d-none');
          hasData = true;
        }

        if (data.t) {
          const date = new Date(data.t * 1000);
          document.getElementById('ctx-time').textContent = date.toLocaleString();
          document.getElementById('row-time').classList.remove('d-none');
          hasData = true;
        }

        if (data.details) {
          document.getElementById('ctx-details').textContent = data.details;

          
          const labelDetails = document.getElementById('label-details');
          if (data.type === 'object') {
            labelDetails.textContent = 'Object Metadata Details:';
          } else if (data.type === 'report') {
            labelDetails.textContent = 'Report Description:';
          } else {
            labelDetails.textContent = 'Firing Context:';
          }

          document.getElementById('row-details').classList.remove('d-none');
          hasData = true;
        }

        if (hasData) {
          document.getElementById('pslens-context-panel').classList.remove('d-none');
        }
      }
    } catch (e) {
      console.error('Failed to parse operational context metadata:', e);
    }
  })();
</script>


## Web Service Operation Access Audit

**Report ID:** `security-ws-access`
**Category:** Integration Broker

## Purpose

This report provides a consolidated view of which PeopleSoft service operations (web services) are accessible, through which permission lists and roles, and how many active (unlocked) users have access through each role. It answers the question: "Who can call our web services and through what security chain?"

## What It Captures

The report traces the full security chain for every service operation authorization:

- **Service Operation** — The web service endpoint (from PSAUTHWS)
- **Permission List** — The permission list granting access to that operation
- **Role** — Each role that includes that permission list
- **Unlocked User Count** — The number of users with that role whose accounts are not locked

## Tables Queried

### PSAUTHWS — Web Service Authorizations

Maps service operations to the permission lists that grant access.

|      Field       |           Description           |
| ---------------- | ------------------------------- |
| IB_OPERATIONNAME | Service operation name (key)    |
| CLASSID          | Permission list granting access |

### PSROLECLASS — Role to Permission List Mapping

Maps roles to their assigned permission lists.

|  Field   |      Description      |
| -------- | --------------------- |
| ROLENAME | Role name (key)       |
| CLASSID  | Permission list (key) |

### PSROLEUSER — Role to User Mapping

Maps roles to users, filtered to unlocked accounts only.

|  Field   |   Description    |
| -------- | ---------------- |
| ROLENAME | Role name (key)  |
| ROLEUSER | User OPRID (key) |

### PSOPRDEFN — User Definitions

Used as a subquery filter to count only unlocked users.

|  Field   |                Description                 |
| -------- | ------------------------------------------ |
| OPRID    | User operator ID (key)                     |
| ACCTLOCK | Account lock status (0=unlocked, 1=locked) |

## Data Flow

```text
1. Bulk fetch ALL PSAUTHWS records (paginated, batches of 300)
   -> Build map: Service Operation -> Permission Lists
        |
        v
2. For each unique Permission List, query PSROLECLASS
   -> Build map: Permission List -> Roles
        |
        v
3. For each unique Role, query PSROLEUSER
   with subquery filter: ACCTLOCK = 0 on PSOPRDEFN
   -> Build map: Role -> Unlocked User Count
        |
        v
4. Flatten into rows and sort by user count (descending)
   -> Generate Markdown report
```

## Report Output

The generated report contains:

- **Summary** with counts of service operations, unique permission lists, and unique roles
- **Access Details Table** with columns: Service Operation, Permission List, Role, Unlocked Users
  - Sorted by unlocked user count (descending) to highlight the most widely accessible operations
  - Permission lists with no roles show "(no roles)" in the Role column
- **Recommendations** for security review

## Interpreting Results

- **High unlocked user counts** on sensitive service operations indicate broad access that may violate least-privilege principles
- **Permission lists with "(no roles)"** are assigned to service operations but not included in any role. They may be orphaned or misconfigured
- **Roles with 0 unlocked users** are granting web service access but have no active users. Candidates for cleanup
- Operations appearing many times (across multiple permission lists and roles) have complex access chains that may be hard to audit manually

## Use Cases

1. **Security audit** — Identify which web services have the broadest user access
2. **Least-privilege review** — Find operations accessible to more users than expected
3. **Cleanup** — Identify permission lists or roles granting web service access with no active users
