Oracle Table Privileges Granted to a Role via role_tab_privs
Oracle Table Privileges Granted to a Role via role_tab_privs
Purpose
ROLE_TAB_PRIVS lists the object privileges granted to roles — every table, view, and sequence grant, the precise privilege type, and whether the right covers the entire object or is scoped to a single column. Oracle's reference notes that "Information is provided only about roles to which the user has access", so run it from an account that holds the roles you are auditing, or use DBA_TAB_PRIVS for every role. It is the correct starting point when you need to answer the question that surfaces in every access review: what table-level access does this role actually allow?
This view is distinct from DBA_TAB_PRIVS, which describes all object grants in the database, to users and roles alike. ROLE_TAB_PRIVS shows only the grants made to roles — the object grants associated with each named role the querying user has access to, regardless of which DBA issued them or when.
The query below takes a role name as a SQL*Plus substitution variable and returns every table-level privilege attached to it. Two extended variants follow: an exact-match form suitable for scripts where interactive prompts are unavailable, and a three-table join that maps which users hold the role and what table access they inherit as a result. All three queries are read-only, touch only data dictionary views, and require no elevated object privileges beyond standard dictionary access.
Code
1-- Core query: all table privileges for a named role
2SELECT owner || '.' || table_name "TABLE"
3 , column_name
4 , privilege
5 , grantable
6FROM role_tab_privs
7WHERE role LIKE '&role'
8/
Exact-match form — suitable for scripts and monitoring queries:
1SELECT owner || '.' || table_name AS table_object
2 , column_name
3 , privilege
4 , grantable
5FROM role_tab_privs
6WHERE role = 'SALES_READ_ONLY'
7ORDER BY owner, table_name, privilege;
Extended audit: users who hold a role and the table privileges they inherit from it:
1SELECT rp.grantee AS db_user
2 , rp.granted_role
3 , tp.owner || '.' || tp.table_name AS table_object
4 , tp.column_name
5 , tp.privilege
6 , tp.grantable
7FROM dba_role_privs rp
8 JOIN role_tab_privs tp
9 ON tp.role = rp.granted_role
10WHERE rp.grantee NOT IN (
11 SELECT role FROM dba_roles
12 )
13ORDER BY rp.grantee, rp.granted_role, tp.owner, tp.table_name;
Nested role chain: roles granted to a role, one level deep:
1SELECT role
2 , granted_role
3 , admin_option
4FROM role_role_privs
5WHERE role = 'SALES_READ_ONLY'
6ORDER BY granted_role;
Code Breakdown
The core ROLE_TAB_PRIVS query
owner || '.' || table_name "TABLE"— concatenates the schema owner and the object name into a fully qualified identifier. Without the owner prefix, a table namedEMPLOYEEScould belong toHR,SCOTT, or any application schema; the qualified form removes that ambiguity.column_name— the column to which the privilege applies. For table-level grants — the common case — this column isNULL. A non-null value indicates a column-level privilege: the role has access only to that specific column, not the full row. Auditors must explicitly check this column; a scan of theprivilegecolumn alone cannot distinguish a table-wideUPDATEgrant from anUPDATE(SALARY)column grant.privilege— the specific right:SELECT,INSERT,UPDATE,DELETE,REFERENCES,ALTER,INDEX, orEXECUTEfor stored objects such as packages and procedures. The same view covers both table and stored-object grants, so anEXECUTErow indicates a procedural grant to the role, not a table grant.grantable— Oracle's reference defines it as "YES if the role was granted with ADMIN OPTION; otherwise NO". It does not meanWITH GRANT OPTION: Oracle allows that option "only when granting to a user or to PUBLIC, not when granting to a role."WHERE role LIKE '&role'— the&rolesubstitution variable prompts for input in SQL*Plus.LIKEallows wildcard searches:SALES%matches every role whose name starts withSALES. Use=instead ofLIKEfor exact-match semantics and to avoid accidental wildcard expansion in scripts./— the forward slash tells SQL*Plus to execute the statement currently in the buffer. It is not a SQL statement terminator; the semicolon in the exact-match variant below serves that role.
The exact-match variant
Replacing LIKE '&role' with = 'SALES_READ_ONLY' removes the substitution variable prompt. ORDER BY owner, table_name, privilege produces deterministic output that can be compared between audit runs with a line-by-line diff — useful for change control and compliance evidence.
The user-to-privilege join
The three-table query connects three privilege layers in one result set: the database user (DBA_ROLE_PRIVS.grantee), the role that user holds (granted_role), and the table privileges that role carries (ROLE_TAB_PRIVS). The NOT IN (SELECT role FROM DBA_ROLES) filter excludes role-to-role grants so the output covers only human accounts. Removing that filter expands the scope to include role-to-role nesting, which is useful when you need the full role inheritance picture.
The nested role chain query
ROLE_ROLE_PRIVS exposes the roles-within-roles structure that ROLE_TAB_PRIVS does not flatten. When ROLE_A is granted to ROLE_B, querying ROLE_TAB_PRIVS for ROLE_B returns only ROLE_B's direct object grants. The ROLE_ROLE_PRIVS query above returns the child roles one level down, so you know which additional roles to query in turn to get the complete privilege picture. For deeply nested structures, iterate through each returned granted_role until no further nesting remains.
Key Points
ROLE_TAB_PRIVSdoes not flatten nested roles. IfROLE_Ais granted toROLE_B, queryingROLE_BreturnsROLE_B's direct table privileges only — not the privilegesROLE_Binherits fromROLE_A. To trace nested grants, queryROLE_ROLE_PRIVSto find the role-to-role chain, then queryROLE_TAB_PRIVSfor each role in the chain.- Column-level grants produce individual rows. A grant of
UPDATE(SALARY)onHR.EMPLOYEESgenerates one row withcolumn_name = 'SALARY'. There is no summary row for the table; every column grant stands alone. A role can hold a full-tableSELECT(one row,column_name NULL) alongside a column-levelUPDATE(a second row,column_name = 'SALARY') on the same table at the same time. - Roles cannot hold
WITH GRANT OPTION. Oracle permits that option only when granting to a user or toPUBLIC, so the grant-option escalation risk sits with users inDBA_TAB_PRIVS(grantable = 'YES'), not with roles. - No rows returned does not mean no access. A role with no table grants can still carry broad access through system privileges such as
SELECT ANY TABLEvisible inROLE_SYS_PRIVS. Always check all three privilege tiers — system, object, and column — when auditing a role. ROLE_TAB_PRIVScovers stored objects too. AnEXECUTEgrant on a package, procedure, or function granted to a role appears here withprivilege = 'EXECUTE', not in a separate view. The same query returns both table grants and executable grants in a single result set.- The
PUBLICrole is a special case. Grants toPUBLICare held by every database user. Query them withSELECT * FROM dba_tab_privs WHERE grantee = 'PUBLIC', the same source this post uses for thePUBLICpath below, becauseROLE_TAB_PRIVSshows only roles to which the querying user has access. On many Oracle installations this set is unexpectedly large and is the first thing to review in a privilege-reduction exercise.
Insights and Best Practices
Audit all three privilege tiers for a complete picture
An Oracle role can carry access through three independent channels: table-level object privileges (ROLE_TAB_PRIVS), system privileges such as CREATE TABLE or SELECT ANY TABLE (ROLE_SYS_PRIVS), and nested role grants (ROLE_ROLE_PRIVS). An audit that reads only ROLE_TAB_PRIVS will miss both a SELECT ANY TABLE system privilege and a nested role that carries table grants. A complete role audit queries all three views and merges the results.
Trace the full user-to-table path
A user's effective table access comes from three sources that must be checked independently: privileges granted directly to the user (DBA_TAB_PRIVS WHERE grantee = :user), privileges carried by roles the user holds (ROLE_TAB_PRIVS joined through DBA_ROLE_PRIVS), and the PUBLIC pseudo-role (DBA_TAB_PRIVS WHERE grantee = 'PUBLIC'). The three-table join in the Code section covers the role-based path. Pair it with a separate query against DBA_TAB_PRIVS for the direct and public grants to produce the full effective privilege set.
Revoke WITH GRANT OPTION wherever it is not required
The grant option cannot sit on a role, but it can sit on users who hold the same objects directly. Check DBA_TAB_PRIVS for grantable = 'YES' rows: each one lets that user pass the privilege on to other users and roles. Oracle's GRANT reference says that to remove the option you revoke the privilege from the user altogether and then grant it again without the grant option.
Use SESSION_ROLES and SESSION_PRIVS to check an active session
When a database session activates a role — either at logon through DEFAULT ROLE or explicitly with SET ROLE — SESSION_ROLES describes the roles that are currently enabled to the user, and SESSION_PRIVS describes the system privileges currently available. Neither lists object privileges, so query them from within a test account to confirm that the expected roles are enabled, then check those roles in ROLE_TAB_PRIVS. They cannot be used for off-session audits but are the fastest way to validate a role configuration during setup.
Handle CDB and PDB scope in multitenant databases
In a multitenant Oracle database (12c and later), ROLE_TAB_PRIVS is container-local: connecting to a PDB returns that PDB's local role grants. Common roles with the C## prefix exist in the CDB root and propagate to all PDBs, but their grants on local PDB objects appear only in that PDB's dictionary, not in the root. An enterprise-wide audit must connect to each PDB in turn and run the query in each container. Grants on common CDB objects appear in the root's dictionary when you connect to CDB$ROOT.
Build a standing audit query with role drift detection
Store a baseline snapshot of ROLE_TAB_PRIVS output for each sensitive role in a comparison table. Schedule a weekly job that re-queries the view, compares the results to the snapshot, and logs any differences as a role-privilege drift event. Newly added rows indicate a privilege was granted since the baseline; missing rows indicate a revoke. This gives a cheap, lightweight change-control record without a third-party security product.
Watch for synonyms masking the real object owner
Oracle's GRANT reference is explicit: "Granting a privilege on a synonym is equivalent to granting the privilege on the base object." So when an application reaches a table through a public or private synonym, look for the base object, not the synonym name, in ROLE_TAB_PRIVS. Query DBA_SYNONYMS on the synonym name to find where it resolves, then verify that the underlying base object is the one the role should have access to.
When to Use This Check
- Baseline role audit before a production deployment or periodic access review.
- Investigating unexpected data access or compliance findings tied to a named role.
- Before modifying or dropping a role, identifying which application functionality depends on its current grants.
- Generating a privilege matrix for SOX, PCI-DSS, or GDPR access-control documentation.
- Comparing role privilege sets across development, test, and production instances to detect environment drift.
- Confirming that a newly created role has been given only the minimal table access it requires.
- Identifying roles granted
WITH ADMIN OPTION(grantable = 'YES') as part of a privilege-reduction initiative.
Troubleshooting Common Issues
If the query returns no rows for a role that clearly exists, the role carries no direct table-level object grants. Run SELECT * FROM role_sys_privs WHERE role = :role to check for system privileges, and SELECT * FROM role_role_privs WHERE role = :role to see whether the role is a container for other roles that hold the actual table grants. A role holding SELECT ANY TABLE would return nothing from ROLE_TAB_PRIVS while still allowing queries on tables, views, and materialized views in any schema except SYS and AUDSYS through a system privilege.
If the substitution variable prompt does not appear, the SQL client may have & substitution disabled or using a different prefix character. Replace LIKE '&role' with = 'ROLENAME' directly, or use a bind variable (:role) if the client supports it.
If grantable shows YES for a privilege you did not expect, Oracle's reference defines that value as "YES if the role was granted with ADMIN OPTION" — it is not a WITH GRANT OPTION grant, which Oracle does not allow on a role. Check DBA_TAB_PRIVS for the original grant record.
If the user-to-privilege join produces duplicate rows, the same user may hold the target role through more than one inheritance path. Add DISTINCT or use GROUP BY (grantee, granted_role, table_object, column_name, privilege) to collapse duplicates while retaining one row per unique access right.
References
- ROLE_TAB_PRIVS — Oracle Database Reference 19c - Authoritative column-by-column definition of the view, including the column_name and grantable fields
- DBA_TAB_PRIVS — Oracle Database Reference 19c - The companion view for direct user-to-object and role-to-object grants; join target for full privilege audits
- ROLE_SYS_PRIVS — Oracle Database Reference 19c - Documents the parallel view for system privileges granted to roles; required alongside ROLE_TAB_PRIVS for a complete role audit
- DBA_ROLE_PRIVS — Oracle Database Reference 19c - Defines the roles-granted-to-users and roles-granted-to-roles view used in the extended three-table audit join
- GRANT — Oracle Database SQL Language Reference 19c - States that WITH GRANT OPTION cannot be granted to a role, and that a grant on a synonym is a grant on the base object
- SESSION_ROLES — Oracle Database Reference 19c - The view of roles currently enabled in the session