AI Action Authorization

Ma Mahalakshmi V Updated 19 Sep 2026
7 min read ·Lesson 162 of 224

User permissions and authorization for AI actions

Lesson 6 of this unit asked "are these tool arguments well-formed and within acceptable bounds?" This lesson asks a different, prior question: is this specific user, in this specific context, allowed to trigger this tool at all? That question is authorization, and it is easy to skip in an AI application because the model's own fluency in following instructions can create a false impression that it also enforces permissions — it does not. The model has no inherent concept of your organization's role hierarchy or your application's access control rules unless your code enforces them independently of anything the model decides.

Authentication vs. authorization

These two terms are frequently conflated but answer different questions:

  • Authentication answers "who is making this request?" — typically resolved before the model is ever involved, through a login session, an API token, or similar.
  • Authorization answers "is this authenticated identity allowed to do this specific thing?" — and this is the question that matters once a tool call is about to execute a real action.

An application can have perfect authentication (it knows exactly which user is talking to the assistant) and still have a serious security gap if it never checks authorization before executing a tool call the model decided to make. Unit 8 introduced tools as the mechanism for connecting a model to real actions; Unit 11 introduced agents that can chain several tool calls autonomously. Neither of those units addressed whether the user behind a given conversation should be allowed to trigger a given tool — that gap is exactly what this lesson closes.

Why the model cannot be the authorization boundary

It's tempting to handle permissions by prompt instruction: "Only allow managers to approve refunds." This can shape behavior, but it is not enforcement, for the same underlying reason Lesson 4 gave for why validation belongs in code rather than in a polite request: prompt instructions are probabilistic guidance, not a hard boundary, and they are exactly the kind of instruction a prompt injection attack (Lesson 4) targets first. If your only refund-approval gate is "the model was told only managers can do this," a cleverly crafted message — from a non-manager user, or embedded in untrusted content the model processes — has a real chance of bypassing it. Authorization must be checked in code, deterministically, using information your application controls (the authenticated user's actual role, looked up from your own user database) — never solely from something the model infers or is told within the conversation.

Role-based access control for tools

A practical, well-understood pattern is role-based access control (RBAC): define a set of roles, and for each tool, define which roles may invoke it. The check happens after the model proposes a tool call but before your code executes it.

from dataclasses import dataclass
from enum import Enum


class Role(str, Enum):
    CUSTOMER = "customer"
    SUPPORT_AGENT = "support_agent"
    MANAGER = "manager"


@dataclass
class User:
    user_id: str
    role: Role


# Maps each tool name to the set of roles allowed to invoke it.
TOOL_PERMISSIONS: dict[str, set[Role]] = {
    "lookup_order_status": {Role.CUSTOMER, Role.SUPPORT_AGENT, Role.MANAGER},
    "issue_refund": {Role.SUPPORT_AGENT, Role.MANAGER},
    "delete_customer_account": {Role.MANAGER},
}


class AuthorizationError(Exception):
    """Raised when a user is not permitted to invoke a given tool."""


def authorize_tool_call(user: User, tool_name: str) -> None:
    allowed_roles = TOOL_PERMISSIONS.get(tool_name)
    if allowed_roles is None:
        # Fail closed: an unrecognized tool name is never authorized,
        # rather than defaulting to "allowed."
        raise AuthorizationError(f"Unknown tool '{tool_name}' has no permission entry")
    if user.role not in allowed_roles:
        raise AuthorizationError(
            f"User role '{user.role.value}' is not permitted to call '{tool_name}'"
        )

The TOOL_PERMISSIONS.get(tool_name) check returning None for an unrecognized tool, and treating that as a rejection rather than a pass, is a deliberate fail-closed design: a new tool added to the system without an accompanying permissions entry is inaccessible by default, rather than accidentally open to everyone. This is the opposite of, and safer than, a "deny list" approach where a tool is available unless explicitly restricted.

def test_customer_can_lookup_order():
    user = User(user_id="u1", role=Role.CUSTOMER)
    authorize_tool_call(user, "lookup_order_status")  # should not raise
    print("PASS: a customer can look up order status")


def test_customer_cannot_issue_refund():
    user = User(user_id="u1", role=Role.CUSTOMER)
    try:
        authorize_tool_call(user, "issue_refund")
        raised = False
    except AuthorizationError:
        raised = True
    assert raised
    print("PASS: a customer is blocked from issuing refunds")


def test_unknown_tool_fails_closed():
    user = User(user_id="u1", role=Role.MANAGER)
    try:
        authorize_tool_call(user, "some_new_tool_without_permissions")
        raised = False
    except AuthorizationError:
        raised = True
    assert raised
    print("PASS: an unregistered tool is rejected even for a manager")


test_customer_can_lookup_order()
test_customer_cannot_issue_refund()
test_unknown_tool_fails_closed()

Authorization must be scoped to the resource, not just the action

Role-based checks answer "can a user with this role ever call this tool?" — but many real applications need a finer-grained check: "can this specific user act on this specific resource?" A support agent might be allowed to issue refunds in general, but only for orders belonging to customers in their assigned region, not for arbitrary orders anywhere in the system. This second layer is often called resource-level or object-level authorization, and it must be checked in addition to the role check, using data about the specific arguments the tool call carries.

@dataclass
class Order:
    order_id: str
    region: str


def authorize_refund_for_order(user: User, order: Order, agent_region: str | None) -> None:
    authorize_tool_call(user, "issue_refund")  # role-level check first
    if user.role == Role.SUPPORT_AGENT and order.region != agent_region:
        raise AuthorizationError(
            f"Agent for region '{agent_region}' cannot refund an order "
            f"in region '{order.region}'"
        )
    # Managers are not region-restricted in this policy.
def test_agent_can_refund_own_region_order():
    agent = User(user_id="a1", role=Role.SUPPORT_AGENT)
    order = Order(order_id="ord_1", region="EU")
    authorize_refund_for_order(agent, order, agent_region="EU")  # should not raise
    print("PASS: an agent can refund an order in their own region")


def test_agent_cannot_refund_other_region_order():
    agent = User(user_id="a1", role=Role.SUPPORT_AGENT)
    order = Order(order_id="ord_2", region="APAC")
    try:
        authorize_refund_for_order(agent, order, agent_region="EU")
        raised = False
    except AuthorizationError:
        raised = True
    assert raised
    print("PASS: an agent cannot refund an order outside their region")


test_agent_can_refund_own_region_order()
test_agent_cannot_refund_other_region_order()

This two-layer structure — role check, then resource-scoped check — mirrors how most real production authorization systems work, and it composes naturally with the tool-argument validation from Lesson 6: validation confirms the arguments are well-formed and in range; authorization confirms this user is allowed to act on this particular resource with them. Both must pass before execution proceeds.

Human-in-the-loop approval for high-risk actions

For actions with significant, hard-to-reverse consequences — deleting an account, issuing a large refund, sending a message to an external party — role-based and resource-scoped authorization can be supplemented with an explicit human confirmation step, regardless of role. This means the tool call is not executed immediately when the model proposes it; instead, your application surfaces the proposed action to a human (the end user, or a supervising staff member) and only executes it after explicit approval.

@dataclass
class PendingAction:
    tool_name: str
    arguments: dict
    requires_confirmation: bool


HIGH_RISK_TOOLS = {"delete_customer_account", "issue_refund"}


def prepare_action(tool_name: str, arguments: dict) -> PendingAction:
    return PendingAction(
        tool_name=tool_name,
        arguments=arguments,
        requires_confirmation=tool_name in HIGH_RISK_TOOLS,
    )

A PendingAction with requires_confirmation=True should be held by your application and only executed after a human explicitly confirms it — through a UI prompt, a confirmation message, or an equivalent step appropriate to your application. This is a deliberate slowdown, and it's justified specifically for actions where an authorization or validation gap, or a successful prompt injection that neither Lesson 4's mitigations nor this lesson's checks fully caught, would otherwise cause irreversible harm.

Common Mistakes

  • Relying on prompt instructions as the sole mechanism for restricting who can trigger a sensitive tool. As with validation, this is guidance the model follows probabilistically, not an enforced boundary, and it is exactly the surface a prompt injection attack targets.
  • Checking role-based permissions but skipping resource-level checks. A support agent authorized to issue refunds "in general" without a per-order check can refund any order in the system, not just ones they're actually responsible for.
  • Defaulting new tools to "allowed" until someone remembers to restrict them. This is a fail-open design; a fail-closed default (Lesson's TOOL_PERMISSIONS.get pattern) is safer because a forgotten permissions entry results in a denial, not an open door.

Best Practices

  • Enforce authorization in code, after the model proposes a tool call and before your code executes it — never treat a prompt instruction as sufficient enforcement.
  • Check both role-level and resource-level authorization for any tool acting on a specific record, account, or entity.
  • Fail closed by default: an unrecognized tool or an ambiguous permission state should be denied, not allowed.
  • Add human-in-the-loop confirmation for high-risk, hard-to-reverse actions, as a layer independent of (not a replacement for) automated authorization checks.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about AI Action Authorization and get answers drawn from it.

Signed-in readers only.