Enforce end-user memory isolation with Deep Data Security
This guide shows how to enforce end-user isolation for an Oracle AI Agent Memory application with OCI IAM and Oracle Deep Data Security.
Warning: Learning environment only. Do not deploy this example to production.
The application uses Flask’s development server, an in-memory session store, environment-file secrets, and simplified token lifecycle handling. The database setup scripts modify database-wide identity-provider settings and privileges. Use only disposable users and an isolated, non-production Oracle Autonomous AI Database.
This guide builds a deliberately small web application:
- Users sign in with the OCI IAM OAuth 2.0 Authorization Code flow.
- The page lists only memories visible to the signed-in user.
- A form adds a memory and exposes its target
username. Alice can store a memory for Alice, but Oracle AI Database rejects Alice’s attempt to store one for Bob.
The route does not compare Alice and Bob itself. It passes the end-user token to Oracle AI Database, where Oracle Deep Data Security enforces the Agent Memory own-row policy.
This guide demonstrates one deployment. For the complete security feature, including every supported policy, principal, administration function, runtime context API, auditing, and revocation timing, see the Deep Data Security API and security reference.
In this guide, you will:
- register a database resource and a web application in an OCI IAM identity domain;
- add the OCI IAM
grouptoken claim used to activate database data roles; - configure separate security-administrator, schema-owner, and application-pool database accounts;
- create an Agent Memory store and grant its own-row policy to an OCI IAM group;
- run the web application and verify database-enforced user isolation.
Understand the trust chain
The browser and application use two different OAuth access tokens:
- The end-user token identifies Alice or Bob and includes the user’s IAM groups.
- The database-access token, obtained by the confidential application through Client Credentials, proves that the application may access the database resource.
Oracle AI Database validates both tokens before attaching the end-user security context.
Oracle documents this two-token model in Understand Authentication Flow and Prerequisites and the complete lifecycle in End-User Security Context.
Prerequisites
You need:
- an isolated Oracle Autonomous AI Database version that supports Oracle Deep Data Security;
- an OCI IAM identity domain in the same tenancy, with permission to manage applications, users, groups, and custom claims;
- an existing database security-administrator account, normally
ADMINon Autonomous AI Database; - SQL*Plus on
PATHand an Autonomous AI Database wallet or TLS connection configuration; - Python 3.10 through 3.14,
uv, and this repository.
Oracle Deep Data Security is database-enforced fine-grained authorization. See What Is Oracle Deep Data Security.
Configure the OCI IAM identity domain
Do all OCI IAM work in one identity domain. Record its Domain URL, for
example https://idcs-<id>.identity.oraclecloud.com:443.
The authoritative Oracle walkthrough is Configure OCI IAM for Application-Mediated Access. The values in the following example match the example environment file supplied with this guide.
Register the database resource
In Identity domain > Integrated applications:
- Add a Confidential Application named
OracleDB. - Configure it as a resource server:
- Primary audience:
OracleDB - Scope:
DB_ACCESS_SCOPE
- Primary audience:
- Configure it as a client and allow Client Credentials.
- Activate it.
- Record its Application ID, Client ID, and Client secret.
The fully qualified database scope is the audience concatenated with the scope name:
OracleDBDB_ACCESS_SCOPE
Follow Register the Database in OCI IAM for the current Console labels and field descriptions.
Register the web application
Add a second Confidential Application named OracleAgentMemoryWeb:
- Enable Enforce grants as authorization.
- Configure it as a resource server:
- Primary audience:
OracleAgentMemoryWeb - Scope:
APP_ACCESS_SCOPE - Access-token lifetime:
3600seconds for this exercise
- Primary audience:
- Configure it as a client with these grants:
- Authorization Code
- Client Credentials
- Add this exact redirect URL:
http://127.0.0.1:8000/auth/callback - Under client resources, grant access to both:
OracleDBDB_ACCESS_SCOPEOracleAgentMemoryWebAPP_ACCESS_SCOPE
- Activate the application and record its Client ID and Client secret.
The application uses Authorization Code with state and PKCE S256. OCI IAM
documents the required client configuration in
Register the Application in OCI IAM
and provides a
PKCE Authorization Code example.
Add the group claim
Oracle AI Database activates externally mapped data roles from the end-user
token’s group claim. OCI IAM custom claims are configured through the
identity-domain REST API, not the Console.
As an Identity Domain Administrator, download a short-lived personal access token that can invoke identity-domain APIs. Then run:
export OCI_IAM_DOMAIN_URL='https://<identity-domain-host>:443'
export OCI_IAM_ADMIN_TOKEN='<short-lived-personal-access-token>'
curl --fail-with-body \
-X POST "$OCI_IAM_DOMAIN_URL/admin/v1/CustomClaims" \
-H "Authorization: Bearer $OCI_IAM_ADMIN_TOKEN" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": [
"urn:ietf:params:scim:schemas:oracle:idcs:CustomClaim"
],
"name": "group",
"value": "$user.groups.*.display",
"expression": true,
"mode": "always",
"tokenType": "AT",
"allScopes": true
}'
A successful request returns HTTP 201. Do not create a duplicate if the
domain already has this claim. See
Configure Custom Claims for Group Information in OCI IAM.
Create Alice, Bob, and the application group
- Create a group named
ORACLEAGENTMEMORY_USERS. - Create test users
aliceandbob. - Assign both users to
ORACLEAGENTMEMORY_USERS. - Assign the group to
OracleAgentMemoryWeb.
The database will later map this group name to a data role. Group names are compared case-insensitively, but use the same spelling throughout the setup. See Create Users and Assign Groups in OCI IAM.
Prepare the environment
Download the complete web application, deepsec_oci_iam_webapp.zip. The archive also contains standalone database setup and inspection scripts owned by this example. They do not import or package scripts from the SDK test suite.
Extract the archive, enter its project directory, and create local setup and runtime environment files:
unzip deepsec_oci_iam_webapp.zip
cd deepsec_oci_iam_webapp
cp deepsec.env.example .deepsec.env
cp deepsec.runtime.env.example .deepsec.runtime.env
python -c "import secrets; print(secrets.token_hex(32))"
Put the generated value in OAM_WEB_SECRET_KEY and populate every required
placeholder in both files. Both filenames are ignored by Git.
The database_scripts directory contains:
| File | Responsibility |
|---|---|
_common.sh |
Private helper that loads .deepsec.env, validates required values, checks for SQL*Plus, and exports TNS_ADMIN when configured. Do not run it directly. |
db_ociiam_setup.sh |
Enables OCI IAM external authentication for the target Autonomous AI Database and replaces OCI_IAM_DOMAIN_DB_CRED$. |
db_deepsec_user_setup.sh |
Creates the schema-owner and app-pool users and grants their documented privilege allowlists. |
db_list_all_data_roles.sh |
Performs read-only inspection of Deep Data Security roles, IAM mappings, data grants, predicates, protected objects, end users, and application identities. |
.deepsec.env is used only by database setup. It includes the security
administrator and schema-owner passwords. .deepsec.runtime.env is loaded by
the Flask process and intentionally excludes both privileged passwords. Do not
export the setup file into the shell that starts the web application. To use
different paths, set OAM_DEEPSEC_ENV_FILE for setup or
OAM_WEB_ENV_FILE for the web process.
The important IAM values map as follows:
| Environment variable | OCI IAM value |
|---|---|
OAM_DEEPSEC_OCI_DB_APP_ID |
Database application’s Application ID |
OAM_DEEPSEC_OCI_DB_CLIENT_ID |
Database application’s Client ID |
OAM_DEEPSEC_OCI_DB_CLIENT_SECRET |
Database application’s Client secret |
OAM_WEB_OCI_CLIENT_ID |
Web application’s Client ID |
OAM_WEB_OCI_CLIENT_SECRET |
Web application’s Client secret |
OAM_WEB_OCI_END_USER_SCOPE |
OracleAgentMemoryWebAPP_ACCESS_SCOPE |
OAM_WEB_OCI_DATABASE_ACCESS_SCOPE |
OracleDBDB_ACCESS_SCOPE |
OAM_DEEPSEC_CONFIG_DIR is needed only when the DSN is a TNS alias whose
tnsnames.ora is not otherwise discoverable. Wallet location and password
are optional when sqlnet.ora and the DSN already provide everything
python-oracledb needs. For Autonomous AI Database connection options, see
Connect Python Applications with a Wallet.
Configure an embedding provider as well. The application only lists and adds memories, but Agent Memory still creates embeddings when it stores memory content. The template uses an OpenAI-compatible model as an example; replace the model, API base, key, and dimension with your provider’s values.
Configure the three database accounts
Warning: The commands in this section change database-wide external-authentication settings and account privileges. Inspect the scripts first. Do not run them against a shared or production database.
Use three distinct database users:
| Responsibility | Example account | Allowed work |
|---|---|---|
| Security administrator | ADMIN |
Enables OCI IAM integration and creates, grants, revokes, lists, and removes data roles and data grants. It is never used for web requests. |
| Managed-schema owner | OAM_SCHEMA_OWNER |
Creates Agent Memory tables, indexes, procedures, and scheduler jobs. It is used only for schema lifecycle operations. |
| App DB user | OAM_APP_DB_USER |
Opens sessions and attaches end-user security contexts. It receives no ordinary SELECT, INSERT, UPDATE, or DELETE privileges. |
This separation makes a request without a valid end-user context fail closed.
Oracle’s database configuration guide recommends CREATE SESSION and
CREATE END USER SECURITY CONTEXT for the connection-pool account; see
Configure the Database for IAM Integration.
The example uses the Autonomous AI Database ADMIN account for setup. A custom
security-administrator account must be able to create and alter the two
database users and grant their listed system privileges. Because the Agent
Memory tables belong to a separate schema, policy administration requires
CREATE ANY DATA GRANT, DROP ANY DATA GRANT, and
ADMINISTER ANY DATA GRANT rather than only CREATE DATA GRANT in the
administrator’s own schema. It must also be authorized to create and drop the
data roles used by the policies.
Enable OCI IAM token validation
Review and run:
bash database_scripts/db_ociiam_setup.sh
The script connects as the security administrator and:
- calls
DBMS_CLOUD_ADMIN.ENABLE_EXTERNAL_AUTHENTICATIONwith the database application’s ID and identity-domain URL; - creates the encrypted
OCI_IAM_DOMAIN_DB_CRED$credential with the database application’s client ID and secret; - prints the resulting identity-provider parameters.
Only one external identity provider can be active. The example script uses
force => TRUE to update OCI IAM parameters, which can disrupt another
external-authentication configuration. Oracle documents the exact Autonomous
Database calls in
Configure the Database for IAM Integration.
Create the owner and pool users
Review and run:
bash database_scripts/db_deepsec_user_setup.sh
The script does not change ADMIN. It creates the owner and application-pool
users and grants:
-- Managed-schema owner
CREATE SESSION, CREATE TABLE, CREATE SEQUENCE,
CREATE VIEW, CREATE PROCEDURE, CREATE JOB
-- Runtime application pool
CREATE SESSION, CREATE END USER SECURITY CONTEXT
The owner receives quota on the Autonomous AI Database DATA tablespace. The
pool user receives no direct privileges on the owner’s tables. To keep the
example focused on the required statements, the script does not check for
existing users or inspect their current privileges. Use new account names; the
script stops if either user already exists or another SQL statement fails.
Create the store and own-row policy
Install the standalone example environment:
uv sync
Then run the setup program:
uv run python scripts/setup_memory.py
This command is intentionally destructive only for
OAM_WEB_MEMORY_STORE_ID. It:
- connects as the schema owner to recreate the Agent Memory schema;
- connects as the security administrator to add the own-row policy and grant
it to
ORACLEAGENTMEMORY_USERS.
The add and grant administration calls are idempotent. Re-running them replaces the same managed policy definitions and assignments, and retrying repairs partial managed DDL left by an interrupted attempt.
The policy creates an externally mapped data role for the OCI IAM group and
data grants whose row predicate compares stored user_id values with
ORA_END_USER_CONTEXT.username. Oracle documents the mapping syntax in
CREATE DATA ROLE
and row authorization in
Create Data Grants.
Inspect the result:
bash database_scripts/db_list_all_data_roles.sh
You should see the IAM-mapped role, the Agent Memory data grants, and their protected owner-schema tables. This inspection script only queries catalog views; it does not create, alter, or remove Deep Data Security objects.
Use an in-database embedding model with Deep Sec
This section applies when the application uses OracleDBEmbedder with the
default provider="database" and an embedding model stored in Oracle AI
Database, typically an imported ONNX model such as DMUSER.DOC_MODEL. In
this configuration, direct embedding uses Oracle’s VECTOR_EMBEDDING SQL
operator, so the model requires the SELECT ON MINING MODEL privilege
described below.
OracleDBEmbedder also supports remote providers through
DBMS_VECTOR_CHAIN.UTL_TO_EMBEDDING, for example with
provider="openai". Those configurations still invoke the embedding
request from Oracle AI Database, but they do not access a database-resident
mining model; the SELECT ON MINING MODEL instructions in this section do
not apply. The Agent Memory operations must still run inside an
OracleMemoryEndUserSecurityContext.
An Agent Memory policy grants access to Agent Memory tables. It does not
grant access to an Oracle AI Database embedding model. The model needs its own
database privilege: SELECT ON MINING MODEL.
Do not grant that privilege to the application-pool user. During a request, Deep Sec authorizes the user through the data role mapped from the OCI IAM group, not through the pool user’s normal privileges. A privilege granted only to the pool user is therefore unavailable while an end-user security context is active.
Create one mapped data role for each IAM group that needs to use the model. Give that data role a normal database role that has access to the model. Run the following SQL as the database security administrator or another account authorized to create roles and grant access to the model. Replace the example names with names for your application:
-- This must exactly match the OCI IAM group passed to OciGroupPrincipal.
CREATE DATA ROLE APP_MEMORY_USERS_DATA_ROLE
MAPPED TO 'IAM_OAUTH_GROUP=YOUR_IAM_GROUP';
-- This ordinary database role carries access to one embedding model.
CREATE ROLE APP_DOC_MODEL_ROLE;
GRANT SELECT ON MINING MODEL YOUR_USER.DOC_MODEL
TO APP_DOC_MODEL_ROLE;
-- Give model access to the IAM group's Deep Sec data role.
GRANT APP_DOC_MODEL_ROLE TO APP_MEMORY_USERS_DATA_ROLE;
The application then uses the same group name when it grants Agent Memory policies:
grant_agent_memory_policies(
admin_connection,
owner_schema="OAM_SCHEMA_OWNER",
memory_store_id="MEMORY",
principals=[OciGroupPrincipal("YOUR_IAM_GROUP")],
policies=[UserOwnRowsDeepDataSecurityPolicy()],
)
grant_agent_memory_policies() finds the existing role mapped to
YOUR_IAM_GROUP and adds the Agent Memory table data grants to that same
role. It does not create a second role. At runtime, a user whose OCI IAM token
contains YOUR_IAM_GROUP can access both the permitted Agent Memory rows and
DMUSER.DOC_MODEL.
If the application already called grant_agent_memory_policies() before
this model setup, Agent Memory has already created the mapped data role. Do
not create another mapped role for the same group. Find the existing role and
grant the ordinary model role to it instead:
SELECT data_role
FROM sys.dba_data_roles
WHERE UPPER(mapped_to) = UPPER('IAM_OAUTH_GROUP=YOUR_IAM_GROUP');
GRANT APP_DOC_MODEL_ROLE TO <DATA_ROLE_RETURNED_BY_THE_QUERY>;
Keep every runtime OracleDBEmbedder operation inside
OracleMemoryEndUserSecurityContext. Do not use the schema owner, a
separate administrator connection, or a connection without an end-user
security context to run embeddings for a user request. That would bypass the
Deep Sec authorization boundary.
Understand the application
The downloadable project, deepsec_oci_iam_webapp.zip, contains the full web application and database scripts. The key application pieces are deliberately small.
Start and complete OAuth
The login route creates random state and PKCE values. Only the opaque
session ID is placed in an HttpOnly, SameSite=Lax cookie; tokens stay
in the process-local demo session store.
def start_authorization(config: RuntimeConfig) -> AuthorizationRequest:
"""Build a state-bound Authorization Code request with PKCE."""
state = secrets.token_urlsafe(32)
code_verifier = secrets.token_urlsafe(64)
challenge = (
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
.rstrip(b"=")
.decode("ascii")
)
query = urllib.parse.urlencode(
{
"client_id": config.oauth_client_id,
"response_type": "code",
"redirect_uri": config.redirect_uri,
"scope": config.end_user_scope,
"state": state,
"code_challenge": challenge,
"code_challenge_method": "S256",
}
)
return AuthorizationRequest(
url=f"{config.domain_url}/oauth2/v1/authorize?{query}",
state=state,
code_verifier=code_verifier,
)
def exchange_authorization_code(
config: RuntimeConfig,
code: str,
code_verifier: str,
) -> EndUserToken:
"""Exchange one browser authorization code for an end-user token."""
payload = _token_request(
config,
{
"grant_type": "authorization_code",
"code": code,
"redirect_uri": config.redirect_uri,
"code_verifier": code_verifier,
},
)
access_token, expires_at = _required_access_token(payload)
return EndUserToken(
access_token=access_token,
username=_display_username(access_token),
expires_at=expires_at,
)
The callback compares state in constant time and exchanges the code with
the same redirect URI and PKCE verifier. The application separately obtains a
database-access token:
def get(self, config: RuntimeConfig) -> str:
"""Return a current database-access token."""
with self._lock:
if self._token is not None and time.time() < self._expires_at - 60:
return self._token
payload = _token_request(
config,
{
"grant_type": "client_credentials",
"scope": config.database_access_scope,
},
)
self._token, self._expires_at = _required_access_token(payload)
return self._token
OCI IAM documents both token requests in Validate the OCI IAM Configuration.
Scope every Agent Memory operation
The application combines both tokens into a python-oracledb end-user security
context. It then scopes every store initialization and operation with
OracleMemoryEndUserSecurityContext:
def list_visible_memories(
config: RuntimeConfig,
pool: Any,
user_context: Any,
) -> list[Any]:
"""List rows visible to the effective OCI IAM end user."""
with OracleMemoryEndUserSecurityContext(user_context):
store = create_runtime_store(config, pool)
return store.list("memory", limit=100)
def add_memory(
config: RuntimeConfig,
pool: Any,
user_context: Any,
content: str,
target_username: str,
) -> None:
"""Attempt to insert a row for the username supplied by the browser."""
with OracleMemoryEndUserSecurityContext(user_context):
store = create_runtime_store(config, pool)
store.add(
contents=[content],
record_type="memory",
user_ids=[target_username],
)
The runtime store uses SchemaPolicy.NO_CHECK because schema creation,
validation, upgrades, and recreation belong to the schema-owner setup path.
The SDK rejects another schema policy while the end-user context is active and
rejects later operations on this protected runtime store when no context is
active. The security-administration functions likewise reject connections
that carry an end-user context.
For each Agent Memory database operation, the context manager:
- acquires a physical connection from the application pool;
- attaches and verifies the intended end-user security context;
- executes SQL under that identity;
- clears and verifies the context before returning the connection.
The pool’s database account does not have fallback table privileges. The python-oracledb payload API is documented in End-User Security Context Payload Creation. The Deep Data Security API and security reference explains the context manager’s complete contract.
Let the database decide
The route forwards the submitted username unchanged. It catches and sanitizes
database errors, but it contains no username == signed_in_user authorization
branch:
@web.route("/", methods=["GET", "POST"])
def index() -> Response | tuple[str, int]:
"""Show visible memories and let the user attempt one insert."""
config, pool, _ = _extensions()
_, session = _session()
if session is None or session.end_user_token is None:
return render_template("index.html", session=None, memories=[])
message = None
status = 200
try:
user_context = _security_context(config, session)
if request.method == "POST":
if not hmac.compare_digest(
request.form.get("csrf_token", ""),
session.csrf_token,
):
return render_template("error.html", message="The form expired."), 400
content = request.form.get("content", "").strip()
target_username = request.form.get("username", "").strip()
if not content or not target_username:
message = "Content and username are required."
status = 400
elif len(content) > 4000 or len(target_username) > 255:
message = "The submitted memory is too large."
status = 400
else:
try:
add_memory(
config,
pool,
user_context,
content,
target_username,
)
message = "Memory added."
except oracledb.DatabaseError:
#Keep listing permitted rows after the deliberately denied write.
message = "Oracle AI Database denied this operation for the effective end user."
status = 403
memories = list_visible_memories(config, pool, user_context)
except oracledb.DatabaseError:
#Never expose raw database errors, token contents, or submitted values.
message = "Oracle AI Database denied this operation for the effective end user."
memories = []
status = 403
except OAuthError:
message = "The login session expired. Sign in again."
memories = []
status = 401
return (
render_template(
"index.html",
session=session,
memories=memories,
message=message,
),
status,
)
Jinja escapes displayed memory content. Raw database exceptions, OAuth responses, tokens, and submitted values are never rendered.
Run and verify the application
Start the Flask development server:
uv run python run.py
Open http://127.0.0.1:8000.
Verify the authorization boundary:
- Sign in as Alice.
- Add
Alice's first memorywith the prefilled usernamealice. - Confirm that the memory appears under Visible memories.
- Add
Alice tries to write for Bobbut change the username tobob. - Confirm that Oracle AI Database denies the operation.
- Sign out, sign in as Bob, and confirm Alice’s memory is not visible.
- Add a memory as Bob, sign back in as Alice, and confirm Bob’s memory is not visible.
This demonstrates two independent controls:
SELECTreturns only rows whoseuser_idmatches the effective identity.INSERTcannot create a row whoseuser_idbelongs to another identity.
For production operations, configure Oracle Unified Auditing for the Deep Sec administration and end-user security-context actions that your organization must retain. Also account for the documented revocation time contract: SDK policy revocation is committed before the administration call returns, while removing a user from an OCI IAM group does not rewrite an already-issued access token. See Deep Data Security for auditing guidance, token-expiry behavior, and links to the corresponding Oracle Deep Sec and OCI IAM documentation.
If login succeeds but the database denies every operation, inspect the
end-user token and verify that group contains
ORACLEAGENTMEMORY_USERS. Oracle’s
Deep Data Security access and privilege troubleshooting
also describes how an administrator can inspect active data roles.
What production still requires
Warning: Completing this guide does not make the example production-ready.
Before adapting the design, replace or add at least:
- a production WSGI server behind TLS, trusted proxy configuration, and secure cookies;
- a durable encrypted server-side session store with expiration, rotation, logout propagation, and concurrency controls;
- a managed secret store instead of dotenv files;
- refresh-token or reauthentication behavior, token revocation handling, and clock-skew-aware expiration;
- rate limiting, request timeouts, audit logging without tokens or user content, and operational monitoring;
- deployment-specific CSRF, Content Security Policy, security headers, input constraints, and error handling;
- migration and policy-change procedures that do not use
RECREATE; - separate deployment identities and network boundaries for administration, schema migration, and runtime;
- high-concurrency, cancellation, context-clearing, and identity-switching tests for the actual server and pool configuration.
Keep the core invariant: the runtime pool account has no direct table DML
privileges, and every Agent Memory request runs inside a verified
OracleMemoryEndUserSecurityContext.