DEV Community

Cover image for I Connected an LLM to Our ERP, and the Permission Model Broke Everything
Aarti Jangid
Aarti Jangid

Posted on

I Connected an LLM to Our ERP, and the Permission Model Broke Everything

Connecting a large language model (LLM) to an enterprise resource planning (ERP) system sounds straightforward at first.

You connect the model to your ERP APIs, provide the relevant business context, define a few tools, and let employees ask questions in natural language.

For example:

“Show me the status of my pending purchase orders.”

“Which invoices are overdue?”

“Summarize inventory levels across our warehouses.”

“Find the customers with outstanding payments.”

The first prototype might work perfectly. The problem starts when you connect it to real users, real business data, and the permission rules that have been protecting that data for years.

I learned an important lesson from this architecture: an LLM should never become a shortcut around the ERP's existing authorization model.

The First Problem Was Not the LLM

Most ERP systems already have a complex permission structure. Access may depend on a user's role, department, assigned business unit, legal entity, location, or record ownership.

A finance employee might view invoices but not employee salaries. A warehouse manager might access inventory for one location but not another. A sales representative might see their own customers but not another team's accounts.

Now introduce an LLM that can call ERP tools.

The model receives a question, determines which tool to call, and generates arguments for that tool. If the backend does not enforce permissions correctly, the model may retrieve records that the user could never access through the normal ERP interface.

For example, consider this simplified tool:

def get_invoice(invoice_id):
return erp_client.get_invoice(invoice_id)

The function works technically. It retrieves an invoice when given a valid ID.

But it has a serious security problem: it does not establish whether the requesting user is allowed to access that invoice.

The model might generate a valid invoice ID based on conversation context, a previous tool result, or an incorrectly exposed identifier. The API call could succeed even when the user has no permission to see the record.

This is not necessarily an LLM failure. It is an authorization failure in the integration layer.

Why Traditional ERP Permissions Become Difficult With AI

In a conventional application, the user interface and backend usually follow a predictable authorization flow. The application knows who is signed in, what operation they requested, and which records they can access.

With an LLM, the request passes through an additional reasoning layer.

The model interprets natural language and selects tools, but it should not decide what the user is authorized to access.

Three common problems appear during implementation.

  1. The model gets more access than the user

A quick prototype often uses one service account with broad ERP permissions.

That makes development easier because the integration can query multiple modules without repeatedly handling access restrictions.

However, if every employee's request uses the same privileged account, the ERP may see all requests as coming from that account rather than from the actual employee.

The result is a confused-deputy problem: a privileged service performs an action on behalf of a less-privileged user without properly enforcing that user's restrictions.

  1. Filtering happens after data retrieval

Another mistake is retrieving a large dataset and asking the model to return only the records relevant to the current user.

For example:

records = erp_client.get_all_invoices()

visible_records = [
record for record in records
if record["department"] == user.department
]

answer = llm.generate(
question=user_question,
context=visible_records
)

Even if the filtering is correct in this simplified example, the application has already retrieved data beyond the user's authorized scope.

If the unfiltered records reach the model, logs, caches, traces, or intermediate processing steps, the exposure has already occurred.

Authorization should constrain the data query itself, not merely the final answer.

  1. Tool calls ignore the user's original identity

An LLM may call multiple tools in one conversation. The first call could retrieve an order, and the next might retrieve its customer details or payment history.

If each tool independently trusts the model's arguments, a user may gain access to related records through an indirect path.

Passing a user_id generated by the model is not sufficient. The identity must come from a trusted authentication context maintained by the application.

The Fix: Put Authorization Between the LLM and the ERP

The architecture that worked better was to treat the LLM as an untrusted request planner.

The model can interpret questions and suggest tool calls. A trusted application layer validates those calls, applies authorization policies, and only then accesses the ERP.

The request flow should look like this:

Authenticated employee
|
v
Application authorization layer
|
v
LLM interprets the request
|
v
Tool argument validation
|
v
ERP permission and record checks
|
v
Authorized data returned
|
v
LLM generates the response

The critical boundary is between tool selection and tool execution.

The model can suggest which invoice to retrieve, but the application must independently verify whether that invoice is accessible to the authenticated employee.

A Safer Python Tool Pattern

Here is a simplified example of enforcing record-level access before returning an invoice.

def get_invoice_for_user(
invoice_id: str,
auth_context
):
# Identity comes from trusted application authentication.
user_id = auth_context.user_id

# Validate the requested identifier.
if not invoice_id or not invoice_id.isalnum():
    raise ValueError("Invalid invoice identifier")

# Query only records accessible to this user.
invoice = erp_client.find_authorized_invoice(
    invoice_id=invoice_id,
    user_id=user_id
)

if invoice is None:
    raise PermissionError(
        "Invoice is unavailable or access is denied"
    )

# Return only fields needed for the current task.
return {
    "invoice_id": invoice.id,
    "status": invoice.status,
    "due_date": invoice.due_date,
    "total": invoice.total
}
Enter fullscreen mode Exit fullscreen mode

This example assumes that find_authorized_invoice() enforces the ERP's actual authorization rules. It is not safe merely because a function has that name; its implementation must apply trusted server-side policy checks.

In production, the authorization context should be created and validated by the application, not supplied or modified by the LLM.

You should also validate tool schemas, use parameterized queries, enforce tenant boundaries, and return only the fields required to answer the question.

What About Role-Based Access Control?

Role-based access control (RBAC) is a useful starting point, but it may not cover every ERP requirement.

A user can have the role finance_manager and still be restricted to a specific legal entity. Another user might access an invoice only if they own the relevant customer account.

A safer design combines:

RBAC: Determines which operations a role can perform.

Attribute-based access control (ABAC): Evaluates attributes such as department, region, legal entity, and ownership.

Record-level authorization: Checks whether a particular invoice, order, customer, or employee record is accessible.

Least privilege: Gives the integration only the permissions needed for its task.

These checks should apply to every sensitive tool call, including indirect calls triggered by earlier results.

The same approach matters when connecting an LLM to CRM software, inventory management, accounting systems, or other enterprise applications.

Don't Forget Prompt Injection and Indirect Access

Even with correct user authentication, an ERP-connected AI assistant can encounter malicious instructions inside documents, purchase orders, customer notes, or imported records.

For example, a record might contain text instructing the model to retrieve confidential payroll information.

That text must be treated as untrusted data, not as an authorization instruction.

Prompt-injection defenses can reduce risk, but they cannot replace backend permissions. Even if the model follows a malicious instruction, the tool execution layer must refuse unauthorized access.

For write operations, such as approving purchase orders or updating customer records, add stricter controls. Depending on the impact, require explicit user confirmation, approval workflows, or separation of duties before executing the action.

How I Would Test the Integration Before Release

I would not rely exclusively on successful demo conversations. I would test the authorization boundaries directly.

My test cases would include:

A user requesting an invoice belonging to another department.

A warehouse manager requesting inventory from an unauthorized location.

A user asking the model to reveal records from another tenant.

A tool call with a fabricated or modified record identifier.

A conversation that attempts to reuse data retrieved under a different user's session.

A prompt injection hidden inside an ERP document.

An employee whose role or permissions change while a session remains active.

An unauthorized write request, such as approving an invoice.

For each test, verify that the backend denies access where appropriate and that sensitive information does not leak through model context, tool outputs, application logs, or error messages.

Also log the authenticated principal, tool name, resource identifier, authorization decision, and request correlation ID. Avoid logging sensitive record contents unnecessarily.

Final Thoughts

Connecting an LLM to an ERP is not just an API integration exercise. It changes how users interact with sensitive business operations, and that makes authorization one of the most important parts of the architecture.

The LLM should help employees express what they need in natural language. It should not determine whether they are allowed to receive it.

Keep identity in the trusted application layer, enforce permissions at the data-access boundary, minimize the information returned to the model, and test unauthorized access as carefully as successful requests.

The best ERP-connected AI assistant is not the one with the most access. It is the one that can answer useful questions without exceeding the user's existing permissions.

Top comments (0)