This is a first-person account from the IT security lead at a 500-person commercial insurance brokerage. Names and a few specific numbers have been generalized at the customer's request. The architecture, configuration, and audit findings are real.
I run security at a mid-sized commercial insurance brokerage. About 500 people, mostly Microsoft 365, with our document estate split across roughly 80 SharePoint sites and a few hundred OneDrive accounts. We sell to small and mid-market businesses, which means we hold a lot of sensitive client information: financial statements, loss runs, ownership disclosures, employee census files. The kind of documents you do not want a chatbot quoting in the wrong context.
When the sales team asked to use Claude and ChatGPT against our SharePoint content, I said yes. Then I went to find out what that actually meant. Here is what I learned.
The Setup
Sales wanted AI for two things: drafting renewal letters from existing policies in the Sales site, and summarizing coverage comparison decks. The platform team had already wired up an MCP server to Microsoft Graph, scoped with delegated OAuth so that each agent inherited the user's permissions.
On paper that sounded reasonable. Each user already had access to what they needed for their job, and the agent would only ever see what the user already saw. The problem with that argument is that most of our salespeople have, over the years, been added as members of more sites than anyone tracks. A senior account executive at our firm can typically reach 40 to 50 sites, including a few they have not touched in years.
That is the user's permission set. AI does not browse the way humans do.
The Audit That Started It
About three months after the rollout, I asked the platform team to dump a week of MCP server logs into Splunk and let me look at what requests were actually being made. I expected most of the traffic to hit the Sales site. It did, but only about 70 percent of it.
The remaining 30 percent of requests were going somewhere else. Some of it was harmless: a senior AE asked an agent to find a particular form template, and the agent searched broadly until it found one. Some of it was not.
In a single week of logs I found:
- An HR record retrieval. An agent had pulled and quoted from a document in the HR site that contained employee compensation ranges. The user had asked, in plain English, to "summarize what we pay account managers." The agent found a planning doc and obliged.
- An M&A folder hit. Our finance team had a private SharePoint folder for a potential acquisition. An agent, prompted to find "the latest deck on the Q4 roadmap," had walked into that folder and read three files.
- A legal-hold email export. One of our salespeople had been told to preserve a thread for litigation, and dropped the export into their personal OneDrive. An agent found it via Microsoft Search a month later.
None of these were data exfiltration to an attacker. The data came back to the same person who asked for it. That is not the point. The point is that AI is a query expansion engine, and an OAuth token sized for a human is way too big for an automated query expansion engine.
I took the report to our CIO on a Friday afternoon. By Monday I had a mandate to fix it without breaking the sales workflow.
What We Tried First
I want to be honest about the false starts because they shaped how we ended up at PortEden.
Sites.Selected
Microsoft Graph supports an app-only permission model called Sites.Selected where an Azure AD app can only access sites a tenant admin has explicitly granted. We tried building a new MCP server on top of that, granting it access to only the Sales site.
It worked for the new MCP server, but only for the new MCP server. The existing delegated-OAuth flow our salespeople were already using kept working with full user permissions. To shut that off, we would have had to revoke the OAuth grants and force everyone through the new server. That meant retraining the team, breaking their existing chat histories, and dealing with a wave of support tickets from people who could no longer find their own files. Sales escalated within an hour of us proposing it.
Sites.Selected is the right answer for net-new integrations. It is not a retrofit.
Sensitivity Labels Alone
We are a Microsoft Information Protection shop. Most of our sensitive content already carries a Confidential or Highly Confidential label. So my next thought was: surely the Graph API respects the labels.
It does not. Or rather, it does report the label on the file, but it does not refuse to return the bytes. The label is metadata, not a permission. An AI agent can read a Highly Confidential document and then truthfully tell you it is Highly Confidential. Configuring the labels to actually block API access would have required an entirely different layer (auto-classification rules with deny actions, plus the Microsoft Purview API integration we had not yet rolled out).
How PortEden Fits In
We found PortEden through one of our security peer groups. The pitch made sense to me on the first call: a token-gated, data-firewalled API surface that sits between AI agents and Microsoft Graph, with rules evaluated on every request. The agent does not talk to Graph directly. It talks to PortEden, and PortEden talks to Graph.
The thing that sold me was the rule taxonomy. Their SharePoint API reference lists rule types for site, drive (document library), folder, list, list item, file ID, MIME type, and sensitivity label. That is the grammar I had been trying to express in DLP rules and getting nowhere. Block rules always win, allow-all and block-all are first class, and the same rules apply to direct reads and to Microsoft Search hits.
Switch-over was a one-day job for the platform team. The MCP server already spoke to a Graph-shaped API, and the PortEden SharePoint API uses the same DTOs as their Drive surface, so most of our existing code paths needed only a base URL change.
The Rules We Set
We did not try to be clever on day one. We started restrictive and opened up where the team complained.
Block-all Default with a Site Allow-list
sharePointAllowAll set to false on every token. Then explicit allow rules with ruleType: site_id for the Sales site, the Marketing site, and the Customer Success site. Three sites. Everything else was implicitly blocked.
That single change cut the audit log by about 80 percent. The HR site, the Finance site, the Legal site, and dozens of personal sites simply stopped responding. Agents got back a clean policy denial they could surface to the user as "I do not have access to that."
Sensitivity-Label Blocks
Even within the three allowed sites, we added block rules with ruleType: sensitivity_label_id for our Highly Confidential and Restricted labels. The label catalogue is tenant-wide, so two rules cover every item carrying those labels, no matter where it lives or moves to. This was the single highest return on investment of any rule we wrote. It works retroactively, and it works on files that get re-classified after the rule is in place.
Field Masking on the Deal Pipeline
Our deal pipeline lives in a SharePoint list with a few hundred rows of in-flight opportunities. We wanted agents to be able to see that the list exists, when it was last updated, and what columns it has, but not to read individual deal notes. The field_values flag in visibleSharePointFields does exactly that. With that flag off, the agent can list and describe the pipeline list but the fields dictionary on each item comes back as null. Sales lost no functionality from this because their workflow was about renewal letters and coverage decks, not opportunity records.
Read-only Operation Flags
We set allowedSharePointOperations to the read_only composite for every token. No upload_file, no create_folder, no write_list_item, no delete_file. If a salesperson wants to upload a draft, they do it through the Office app, like before. AI is for reading and drafting, not for mutating the document estate. We may revisit this later for specific workflows, but the cost of the default-read-only posture is essentially zero.
Per-Team Tokens, Not One Master Key
One thing I want to highlight because it was non-obvious to me at first: we issued one token per team, not one per user, and not one master token. The Sales token has Sales+Marketing+Customer Success allowed. The Legal team got a separate token with Legal site access only and the same label blocks. If a token leaks or has to be revoked, the blast radius is one team, not the whole company.
Six Weeks Later
I pulled the audit log again last week. The numbers I care about:
- Zero successful reads from blocked sites in six weeks of operation.
- 147 policy denials across all tokens, of which I have so far reviewed about 30. Roughly half were genuine attempts the user did not realize they were making (an agent walking up the search results into a blocked site). The other half were near-misses I am glad we caught: an attempt to read a labelled HR planning document, an attempt to summarize a Legal folder, an attempt to fetch a Highly Confidential M&A deck from the same folder I had flagged in the original audit.
- Drafting time on renewal letters dropped from roughly 30 minutes to roughly 4 minutes per letter, according to the sales lead. None of that productivity came from access to restricted sites. It all came from access to the Sales site, which is what we wanted in the first place.
- Support tickets from sales: zero. The team did not notice the change because their actual workflow never touched the blocked sites.
That last bullet is the one I find most reassuring. The right security boundary should be invisible to the people doing legitimate work, and noisy only when something goes wrong.
What I Would Do Differently
A few things I want to flag for anyone running the same playbook.
Start with block-all. It is easier to add allow rules in response to real user requests than to subtract from a permissive default. The team will tell you exactly what they need. They will not, however, notice the dozens of things the agent was reaching for that they did not need.
Use sensitivity_label_id rules early. They scale better than file-level rules, they work retroactively, and they keep working when documents move. A single label rule is worth a hundred file-ID allow lists.
Audit Microsoft Search separately. KQL through Graph is wider than people expect. Confirm with your provider (PortEden does this) that search hits are filtered through the same rules as direct reads. Otherwise search becomes the back door.
Issue per-team tokens. One master token feels convenient. It is a foot-gun. The blast radius of a single incident is the difference between "rotate one token" and "rotate everything."
Keep the audit log review on a calendar. Weekly for the first month, monthly thereafter. The denials list is the single most valuable artifact you will get out of this. It is the thing your auditor and your CIO will both want to see.
AI access to SharePoint is now boring infrastructure for us, which is what you want. Sales gets faster drafts. I get a clean audit log. The Highly Confidential folder stays Highly Confidential.
If you are about to do this, do the audit first. Connect AI in a small pilot, log every Graph request for two weeks, and read the logs before you scale up. You will be surprised. We were.