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:

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:

Understand the trust chain

The browser and application use two different OAuth access tokens:

  1. The end-user token identifies Alice or Bob and includes the user’s IAM groups.
  2. 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:

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:

  1. Add a Confidential Application named OracleDB.
  2. Configure it as a resource server:
    • Primary audience: OracleDB
    • Scope: DB_ACCESS_SCOPE
  3. Configure it as a client and allow Client Credentials.
  4. Activate it.
  5. 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:

  1. Enable Enforce grants as authorization.
  2. Configure it as a resource server:
    • Primary audience: OracleAgentMemoryWeb
    • Scope: APP_ACCESS_SCOPE
    • Access-token lifetime: 3600 seconds for this exercise
  3. Configure it as a client with these grants:
    • Authorization Code
    • Client Credentials
  4. Add this exact redirect URL:
    http://127.0.0.1:8000/auth/callback
  5. Under client resources, grant access to both:
    • OracleDBDB_ACCESS_SCOPE
    • OracleAgentMemoryWebAPP_ACCESS_SCOPE
  6. 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

  1. Create a group named ORACLEAGENTMEMORY_USERS.
  2. Create test users alice and bob.
  3. Assign both users to ORACLEAGENTMEMORY_USERS.
  4. 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:

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:

  1. connects as the schema owner to recreate the Agent Memory schema;
  2. 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:

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:

  1. Sign in as Alice.
  2. Add Alice's first memory with the prefilled username alice.
  3. Confirm that the memory appears under Visible memories.
  4. Add Alice tries to write for Bob but change the username to bob.
  5. Confirm that Oracle AI Database denies the operation.
  6. Sign out, sign in as Bob, and confirm Alice’s memory is not visible.
  7. Add a memory as Bob, sign back in as Alice, and confirm Bob’s memory is not visible.

This demonstrates two independent controls:

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:

Keep the core invariant: the runtime pool account has no direct table DML privileges, and every Agent Memory request runs inside a verified OracleMemoryEndUserSecurityContext.