PL/SQL & T-SQL (Oracle and SQL Server) Module 13 – Dynamic SQL and security in PL/SQL
Definer's rights, invoker's rights and roles
Choose AUTHID DEFINER or AUTHID CURRENT_USER for PL/SQL units, see how roles behave in each, and give callers EXECUTE on an API instead of its tables.
What you will learn
- Explain what AUTHID DEFINER and AUTHID CURRENT_USER change about name resolution and privilege checks
- Protect tables by granting EXECUTE on an API package instead of privileges on the tables
- Use code-based access control and ACCESSIBLE BY to limit what a unit may do and who may call it
- Design a least-privilege layout with separate data, API and application schemas
Before you start
What you’ll learn · Before you start
What you will learn
- Explain what AUTHID DEFINER and AUTHID CURRENT_USER change about name resolution and privilege checks
- Protect tables by granting EXECUTE on an API package instead of privileges on the tables
- Use code-based access control and ACCESSIBLE BY to limit what a unit may do and who may call it
- Design a least-privilege layout with separate data, API and application schemas
Before you start
On this page
LAB’s procedure balance_dr prints 760.25 for APP_USER, a user who may not read LAB’s accounts table at all, while the
same query in balance_ir fails for that user with ORA-00942. One clause decides which happens: the unit’s AUTHID,
which sets whose schema its names resolve in and whose privileges are checked.
You will predict both outcomes, protect tables with an EXECUTE grant on an API, and keep every caller to the least
it needs.
AUTHID decides whose schema and privileges a unit uses
Oracle’s AUTHID page says the property affects how the SQL a unit issues at run time resolves names and checks
privileges; it does not affect compilation. AUTHID DEFINER, the default, makes a unit run with
definer's rights: The default rights model of a PL/SQL unit, AUTHID DEFINER: while it runs, names resolve in its owner's schema and the owner's privileges are checked, with only the role PUBLIC enabled.: when it is called, CURRENT_USER and CURRENT_SCHEMA become its
owner and only the role PUBLIC stays enabled. AUTHID CURRENT_USER, which the page calls the preferred usage,
gives invoker's rights: A rights model where a subprogram runs with the privileges of the session that calls it, set by AUTHID CURRENT_USER. The alternative, AUTHID DEFINER, runs with the privileges of the user who created it. More in “PL/SQL procedures, functions and parameters”: nothing switches, so names resolve in the caller’s schema and
the caller’s privileges and enabled roles are checked. So the owner’s roles count for nothing inside a definer’s-rights
unit, while the caller’s roles do count inside an invoker’s-rights unit.
Step through APP_USER’s two calls, one to each kind of procedure:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A caller with EXECUTE only. APP_USER may connect and has EXECUTE on LAB's two procedures. It holds no privilege on any LAB table, so a plain SELECT from lab.accounts fails with ORA-00942.
- The definer's-rights call. balance_dr is AUTHID DEFINER, the default. While it runs, CURRENT_USER and CURRENT_SCHEMA become LAB and only the role PUBLIC is enabled.
- It reads LAB's table as LAB. The name accounts resolves in LAB's schema, LAB owns the table, and the call prints 760.25. The caller reached data it could not read itself, but only through this procedure.
- The invoker's-rights call. balance_ir is AUTHID CURRENT_USER. Nothing switches, so it runs as APP_USER, and accounts resolves in APP_USER's own schema.
- The caller has no such table. APP_USER owns no table named accounts, so the call fails with ORA-00942. Naming lab.accounts would not help either, because APP_USER may not read that table.
The model follows the same rules for four calls and a grant. Press Edit, add a grant to GRANTS or a call to
CALLS, and run it again:
"""Whose rights a PL/SQL unit runs with, and where a name such as accounts resolves.
LAB owns the practice table accounts and procedures that read it: balance_dr is AUTHID DEFINER, and
balance_ir and balance_ir_lab (which names lab.accounts) are AUTHID CURRENT_USER. APP_USER holds only
EXECUTE on them. The model follows Oracle's rules for each call: a definer's-rights unit switches
CURRENT_USER and CURRENT_SCHEMA to its owner and enables only the role PUBLIC; an invoker's-rights unit
keeps the caller's. It resolves the name, checks the privilege and prints the outcome. rights_demo.sql
makes the same calls in the database. Edit CALLS or GRANTS and run it again.
"""
TABLES = {("LAB", "ACCOUNTS"): {3: "760.25"}} # (schema, table): {account_id: balance}
UNITS = { # unit: (owner, AUTHID, the table name its query uses)
"LAB.BALANCE_DR": ("LAB", "DEFINER", "accounts"),
"LAB.BALANCE_IR": ("LAB", "CURRENT_USER", "accounts"),
"LAB.BALANCE_IR_LAB": ("LAB", "CURRENT_USER", "lab.accounts"),
}
GRANTS = {("APP_USER", "EXECUTE", "LAB.BALANCE_DR"), ("APP_USER", "EXECUTE", "LAB.BALANCE_IR"),
("APP_USER", "EXECUTE", "LAB.BALANCE_IR_LAB")}
def can(user, privilege, obj):
return obj.split(".")[0] == user or (user, privilege, obj) in GRANTS # owners hold every privilege
def call(session_user, unit, account_id):
owner, authid, name = UNITS[unit]
if not can(session_user, "EXECUTE", unit):
return session_user, "-", "PLS-00201: identifier must be declared"
# DEFINER: the owner's identity and schema, only PUBLIC enabled; CURRENT_USER: the caller's, unchanged.
current_user = owner if authid == "DEFINER" else session_user
schema, table = name.upper().split(".") if "." in name else (current_user, name.upper())
target = f"{schema}.{table}"
if (schema, table) not in TABLES or not can(current_user, "SELECT", target):
# Oracle gives the same error for a table that is missing and one the user may not read.
return current_user, target, f'ORA-00942: table or view "{schema}"."{table}" does not exist'
return current_user, target, f"balance {TABLES[(schema, table)][account_id]}"
CALLS = [
("APP_USER", "LAB.BALANCE_DR"),
("APP_USER", "LAB.BALANCE_IR"),
("APP_USER", "LAB.BALANCE_IR_LAB"),
("LAB", "LAB.BALANCE_IR"),
]
print("Each call reads the balance of account 3")
for who, unit in CALLS:
current_user, target, outcome = call(who, unit, 3)
print(f" {who:<8} calls {unit:<18} runs as {current_user:<8} reads {target:<16} -> {outcome}")
GRANTS.add(("APP_USER", "SELECT", "LAB.ACCOUNTS"))
print("After GRANT SELECT ON lab.accounts TO app_user")
for who, unit in [("APP_USER", "LAB.BALANCE_IR_LAB")]:
current_user, target, outcome = call(who, unit, 3)
print(f" {who:<8} calls {unit:<18} runs as {current_user:<8} reads {target:<16} -> {outcome}") Output
Each call reads the balance of account 3 APP_USER calls LAB.BALANCE_DR runs as LAB reads LAB.ACCOUNTS -> balance 760.25 APP_USER calls LAB.BALANCE_IR runs as APP_USER reads APP_USER.ACCOUNTS -> ORA-00942: table or view "APP_USER"."ACCOUNTS" does not exist APP_USER calls LAB.BALANCE_IR_LAB runs as APP_USER reads LAB.ACCOUNTS -> ORA-00942: table or view "LAB"."ACCOUNTS" does not exist LAB calls LAB.BALANCE_IR runs as LAB reads LAB.ACCOUNTS -> balance 760.25 After GRANT SELECT ON lab.accounts TO app_user APP_USER calls LAB.BALANCE_IR_LAB runs as APP_USER reads LAB.ACCOUNTS -> balance 760.25
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rights_model.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Only the definer’s-rights call reads the balance for APP_USER. balance_ir_lab names lab.accounts, so it finds the
table, yet still fails with ORA-00942, because Oracle reports that error both for a missing table and for one the
user may not read. A trigger always behaves as definer’s rights and an anonymous block as invoker’s rights. For
the dynamic SQL of the previous lesson, both steps happen once,
at run time; static SQL is also checked at compile time, as if the unit were definer’s rights.
The script makes the same calls on the practice schema as APP_USER, then a direct SELECT that fails the same way:
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; PDB FREEPDB1. Client: SQL*Plus or SQLcl.
-- Definer's rights against invoker's rights for a second user. Step 1 runs as an administrator of the lab PDB,
-- step 2 as LAB, step 3 as APP_USER. The password is asked for at run time with ACCEPT ... HIDE, never written in the
-- script and never shown: SET VERIFY OFF keeps SQL*Plus from printing the line with the value put in.
-- examples/model/rights_model.py prints the same outcomes.
SET SERVEROUTPUT ON
SET VERIFY OFF
-- Step 1, as an administrator: a second user who may only connect.
ACCEPT app_user_password CHAR PROMPT 'Password for APP_USER: ' HIDE
CREATE USER app_user IDENTIFIED BY "&app_user_password";
GRANT CREATE SESSION TO app_user;
-- Step 2, as LAB: three procedures that read the balance of one account.
CREATE OR REPLACE PROCEDURE balance_dr (p_id IN NUMBER) AUTHID DEFINER IS
v_balance accounts.balance%TYPE;
BEGIN
SELECT balance INTO v_balance FROM accounts WHERE account_id = p_id; -- runs as LAB: LAB.ACCOUNTS
DBMS_OUTPUT.PUT_LINE('balance ' || v_balance);
END;
/
CREATE OR REPLACE PROCEDURE balance_ir (p_id IN NUMBER) AUTHID CURRENT_USER IS
v_balance accounts.balance%TYPE;
BEGIN
SELECT balance INTO v_balance FROM accounts WHERE account_id = p_id; -- runs as the caller: caller.ACCOUNTS
DBMS_OUTPUT.PUT_LINE('balance ' || v_balance);
END;
/
CREATE OR REPLACE PROCEDURE balance_ir_lab (p_id IN NUMBER) AUTHID CURRENT_USER IS
v_balance lab.accounts.balance%TYPE;
BEGIN
SELECT balance INTO v_balance FROM lab.accounts WHERE account_id = p_id; -- LAB.ACCOUNTS, caller's privileges
DBMS_OUTPUT.PUT_LINE('balance ' || v_balance);
END;
/
GRANT EXECUTE ON balance_dr TO app_user;
GRANT EXECUTE ON balance_ir TO app_user;
GRANT EXECUTE ON balance_ir_lab TO app_user;
-- Step 3, as APP_USER, who holds no privilege on any LAB table.
EXEC lab.balance_dr(3)
-- Expected: balance 760.25
EXEC lab.balance_ir(3)
-- Expected: ORA-00942: table or view "APP_USER"."ACCOUNTS" does not exist
EXEC lab.balance_ir_lab(3)
-- Expected: ORA-00942: table or view "LAB"."ACCOUNTS" does not exist (APP_USER may not read it)
SELECT balance FROM lab.accounts WHERE account_id = 3;
-- Expected: ORA-00942 as well: EXECUTE on balance_dr gave APP_USER no access to the table itself
-- Step 4, as an administrator, once APP_USER has disconnected: remove the lab user.
DROP USER app_user CASCADE; """rights_demo.sql in the lab, step by step, each in the client it names: an administrator (SYS in FREEPDB1),
LAB and APP_USER. The password of APP_USER is made for the run and given to the script's ACCEPT; it is
never shown."""
from oracle_session import Lab
STEP = r"^-- Step \d+, as (?P<who>an administrator|LAB|APP_USER)\b"
WHO = {"an administrator": ["admin"], "LAB": ["lab"], "APP_USER": ["app_user"]}
with Lab() as lab:
typed = {"app_user_password": lab.new_password("app_user")}
lab.steps("rights/rights_demo.sql", STEP, WHO, users={"admin": "sys", "lab": "lab", "app_user": "app_user"},
typed=typed, close={4: ["app_user"]}, errors=3) Output
admin> Step 1, as an administrator: a second user who may only connect.
User created.
Grant succeeded.
lab> Step 2, as LAB: three procedures that read the balance of one account.
Procedure created.
Procedure created.
Procedure created.
Grant succeeded.
Grant succeeded.
Grant succeeded.
app_user> Step 3, as APP_USER, who holds no privilege on any LAB table.
balance 760.25
PL/SQL procedure successfully completed.
BEGIN lab.balance_ir(3); END;
*
ERROR at line 1:
ORA-00942: table or view "APP_USER"."ACCOUNTS" does not exist
ORA-06512: at "LAB.BALANCE_IR", line 4
ORA-06512: at line 1
Help: https://docs.oracle.com/error-help/db/ora-00942/
BEGIN lab.balance_ir_lab(3); END;
*
ERROR at line 1:
ORA-00942: table or view "LAB"."ACCOUNTS" does not exist
ORA-06512: at "LAB.BALANCE_IR_LAB", line 4
ORA-06512: at line 1
Help: https://docs.oracle.com/error-help/db/ora-00942/
SELECT balance FROM lab.accounts WHERE account_id = 3
*
ERROR at line 1:
ORA-00942: table or view "LAB"."ACCOUNTS" does not exist
Help: https://docs.oracle.com/error-help/db/ora-00942/
admin> Step 4, as an administrator, once APP_USER has disconnected: remove the lab user.
User dropped.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rights_demo_session.py
Checkpoint
Check: What AUTHID DEFINER and CURRENT_USER change
Grant EXECUTE on an API, never the tables behind it
The rights model turns into a design rule. APP_USER needs to read balances and make deposits, so it gets EXECUTE on
one definer’s-rights package and no privilege on any table. The package checks every request, refusing a deposit that
is not positive, and a direct UPDATE of the table from APP_USER fails with ORA-00942. This is
least privilege: The rule that each user, login or role holds the rights its job needs and no others, so a bug or a stolen password can do as little as possible. More in “Security for T-SQL code: permissions and chains” in practice: a bug or a stolen password in the application can do
only what the package allows. Answer two questions about a unit to choose its AUTHID:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| AUTHID | Whose tables names reach | Whose privileges count | What to watch |
|---|---|---|---|
| Definer's rights Written as: AUTHID DEFINER | Good the owner's, the same for every caller | Mixed the owner's direct grants, with only PUBLIC enabled | Mixed callers act with the owner's power, so keep it small |
| Invoker's rights Written as: AUTHID CURRENT_USER | Good the caller's own schema | Good the caller's, as if the caller ran the SQL | Mixed every caller needs privileges on what it reads |
How the answers decide
- “Should callers change data they may not read or change themselves?” Yes, only through this unit: rules out Invoker's rights. Only a definer's-rights unit lets a caller reach the owner's data, and only in the ways the unit allows.
- “Should callers change data they may not read or change themselves?” No, only what they already may and “Should each caller work on the tables of their own schema?” Yes, one copy per schema: rules out Definer's rights. An invoker's-rights unit resolves names in the caller's schema and checks the caller's own privileges.
- “Should callers change data they may not read or change themselves?” No, only what they already may and “Should each caller work on the tables of their own schema?” No, one shared set of tables: adds a caveat to Definer's rights. An invoker's-rights unit that names the shared tables keeps every caller to the privileges they already hold.
Several tools tighten the pattern further. A role granted to the package itself, called
code-based access control: Granting a database role to a PL/SQL function, procedure or package itself, so that the role is enabled while that unit runs and the unit holds privileges its callers do not., gives a unit privileges that no user holds.
ACCESSIBLE BY: A clause that lists the PL/SQL units allowed to call a package or one of its subprograms; any other caller is refused with PLS-00904, whatever privileges it holds. More in “PL/SQL packages, session state and ORA-04068” limits which units may call a package. A layout of separate data, API
and application schemas keeps each account to one job. DB_DEVELOPER_ROLE: A predefined Oracle role, from Oracle Database 23ai, that gives application developers most of the privileges they need to build and deploy applications, offered as a narrower choice than the DBA role. gives
developers what they need without DBA, and a schema privilege grants one system privilege on a single schema.
Key idea
A definer’s-rights API is a door that opens with the owner’s keys. Give callers EXECUTE on the door, never
privileges on the tables behind it, and keep the door’s own rules inside the package.
Checkpoint
Check: EXECUTE on an API instead of the tables
Roles, code-based access control and ACCESSIBLE BY, in seven cases Pro: opens with a Pro, Premium or Ultimate pass
A definer’s-rights package that gets its privileges through a role fails in two different ways, and a role granted to
the package itself repairs one of them. Seven cases on a three-schema copy of the practice data show each rule, with
ACCESSIBLE BY closing the door on direct calls.
In this part:
- Why a role does not reach inside a definer’s-rights unit, worked through four cases
- Granting a role to the unit itself
- ACCESSIBLE BY: only the listed units may call
3 runnable examples (Python, SQL) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
A least-privilege layout: data, API and application schemas Pro: opens with a Pro, Premium or Ultimate pass
The same practice data can be split across three schemas so that the application’s account can reach the tables only through one package. The grants, the roles for developers and reports, and the things that never belong in PL/SQL source are set out below.
In this part:
- Three schemas and one door, worked through
- A developer role and schema privileges instead of DBA and ANY
- A dictionary check that fails on a weak layout
- What stays out of PL/SQL source
2 runnable examples (Python, SQL) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: eight privilege puzzles
Each puzzle gives a caller, a unit with its AUTHID and the grants in force. Decide what the call returns, then read
why each answer is right or wrong:
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (2–5 years): what changes when a procedure is created AUTHID CURRENT_USER?
Its SQL runs with the caller’s rights instead of the owner’s. Unqualified names resolve in the caller’s schema, and
privileges are checked against the caller and the caller’s enabled roles. The default, AUTHID DEFINER, switches to
the owner and enables only PUBLIC while the unit runs.
Three longer questions follow, on a procedure that fails for one caller, a package whose grant came through a role, and a schema layout to design. Answer each in your own words first; its first step shows where a strong answer starts:
-
Mid to senior
A utility procedure in schema TOOLS is created AUTHID CURRENT_USER and selects from a table named settings. It works when TOOLS runs it but fails with ORA-00942 for every other user who holds EXECUTE on it. Explain why, and give two fixes, saying when each is right.
-
Mid to staff
A definer's-rights package compiles in development, where its owner holds SELECT on a table directly. In test, the same privilege was granted to the owner through a role, and the package body does not compile. A colleague then rewrites the query as dynamic SQL, which compiles but fails at run time. Explain both failures and the right fix.
-
Senior to staff
Design the database accounts for a small banking application on the practice tables. The application server must read balances and make deposits, analysts must query every table, and developers must deploy code. Who owns what, and what does each account hold?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does a unit's AUTHID affect, and what does it not?
How the SQL the unit issues at run time resolves names and checks privileges. It does not affect compilation.
-
What happens when a definer's-rights unit is called?
AUTHID DEFINER is the default: CURRENT_USER and CURRENT_SCHEMA become the unit's owner and only the role PUBLIC stays enabled, so the owner's roles count for nothing inside it.
-
What does AUTHID CURRENT_USER give a unit?
Invoker's rights, which Oracle calls the preferred usage: nothing switches, so names resolve in the caller's schema and the caller's privileges and enabled roles are checked.
-
An invoker's-rights unit names lab.accounts and still fails with ORA-00942. Why?
Oracle reports that error both for a table that does not exist and for one the user may not read, so naming the owner finds the table but the caller's missing privilege still refuses it.
-
Which rights model does a trigger use, and which does an anonymous block use?
A trigger always behaves as definer's rights and an anonymous block as invoker's rights.
-
When are a unit's names resolved and privileges checked for dynamic and for static SQL?
Dynamic SQL is resolved and checked once, when it runs. Static SQL is also checked when the unit compiles, with the privileges a definer's-rights unit would have.
-
What does an application user get instead of rights on the tables?
EXECUTE on one definer's-rights package and no privilege on any table: the package checks every request, and a direct UPDATE from the application fails with ORA-00942, so a bug or a stolen password can do only what the package allows.
-
Which four tools tighten the pattern further?
Code-based access control, a role granted to the package itself, which gives a unit privileges no user holds; ACCESSIBLE BY, which limits the units allowed to call it; separate data, API and application schemas; and narrow roles, such as DB_DEVELOPER_ROLE instead of DBA, with schema privileges granting one system privilege on a single schema.
Key takeaways
AUTHID DEFINER, the default, runs a unit as its owner with onlyPUBLICenabled, andAUTHID CURRENT_USERruns it as the caller with the caller’s roles.- Names in a unit’s SQL resolve in the schema whose rights apply, so an invoker’s-rights unit looks for the caller’s tables, and ORA-00942 covers both a missing table and one the caller may not read.
- Callers get
EXECUTEon a definer’s-rights API and no privilege on its tables, so the package’s own checks hold for every request. - Roles granted to units,
ACCESSIBLE BY, separate schemas and narrow roles for developers and reports all keep each account to the least it needs.
Terms in this lesson
- code-based access control
- Granting a database role to a PL/SQL function, procedure or package itself, so that the role is enabled while that unit runs and the unit holds privileges its callers do not.
- DB_DEVELOPER_ROLE
- A predefined Oracle role, from Oracle Database 23ai, that gives application developers most of the privileges they need to build and deploy applications, offered as a narrower choice than the DBA role.
- definer's rights
- The default rights model of a PL/SQL unit, AUTHID DEFINER: while it runs, names resolve in its owner's schema and the owner's privileges are checked, with only the role PUBLIC enabled.
References
- Invoker's Rights and Definer's Rights (AUTHID Property) (Oracle)
- Invoker's Rights and Definer's Rights Clause (Oracle)
- About Definer's Rights and Invoker's Rights (Security Guide) (Oracle)
- Using Code Based Access Control for Definer's Rights and Invoker's Rights (Oracle)
- ACCESSIBLE BY Clause (Oracle)
- Considerations for Using Application-Based Security (DB_DEVELOPER_ROLE) (Oracle)
- Managing System Privileges (schema privileges) (Oracle)
- ORA-00942 (Database Error Messages) (Oracle)
Report a problem with this lesson
Kept only in this browser. Your Learn progress