Check what each user can see
By the end of this lesson, you can show that a user gets only the passages of documents that ADM lets them see, explain why a restricted user sees the answer 'nothing found', and read in the trace tables what the agent searched for and what it got back.
Where the access rule is enforced
Section titled “Where the access rule is enforced”This lesson continues with the agent and the chat page of lessons 2 to 4. The two users of the course have different rights:
| Document | AIC_LEGAL |
AIC_HR |
|---|---|---|
| The Helios distribution contract | can see | cannot see |
| The salary bands | cannot see | can see |
The rule is not in the prompt and not in the tool. adm_ai_rag_api.prepare_sources calls search_chunks, which checks every chunk with adm_access_control_api.user_is_allowed_to_view_document_personal for the user in the ADM context. “Personal” means without the administrator bypass: an administrator also sees only the documents that they own or that are shared with them.
The check uses the folder rights, the group membership and the shares of ADM. A model cannot talk its way past it, because the model never sees a chunk of a document that the check refused.
A refusal of the model proves nothing about the tool. To prove the rule, call the tool function directly, as each user, without a model.
Run @12_aic_two_users.sql. Part A calls aic_rag_tools_api.search as each user:
adm_context_api.system_user_login(p_user);l_out := aic_rag_tools_api.search(p_collection, p_query);=== Part A: the same search, called as each user (no agent) ===AIC_LEGAL limit default threshold default -> ok contract_helios_distribution.pdf (.667)AIC_HR limit default threshold default -> no_passagesAIC_HR limit default threshold default -> ok salary_bands.pdf (.617)AIC_LEGAL limit default threshold default -> no_passagesThe first two lines search the contracts with the words “Helios exclusive distribution notice period twelve months”. The Legal user finds the Helios contract, and the HR user gets the status no_passages. The last two lines search the policies for “salary band senior engineer”, and the result is the other way round.
The tool has no access code of its own: it calls prepare_sources and reports what comes back.
See how the search skips documents the user cannot read
Section titled “See how the search skips documents the user cannot read”Part B calls the same search with the score threshold 0.3 and with small values for p_limit.
The query “exclusive distribution Austria Switzerland” has two best chunks, and both belong to the Helios contract. AIC_HR cannot see that contract.
The search does not stop at the p_limit best chunks. It first takes three times as many candidates, and runs the access check on them in rank order. It keeps the first p_limit chunks that the user can see.
If too few chunks remain, the search doubles the number of candidates and tries again. The number of candidates stops at 20 times p_limit, or at 500 chunks if that is less. The search also stops when no more chunks reach the score threshold.
=== Part B: p_limit. The search skips chunks the user may not read and takes more candidates ===(query "exclusive distribution Austria Switzerland": the two best chunks are both from the Helios contract)AIC_LEGAL limit 1 threshold .3 -> ok contract_helios_distribution.pdf (.596)AIC_HR limit 1 threshold .3 -> ok contract_ferrotek_framework.pdf (.386)AIC_HR limit 2 threshold .3 -> ok contract_ferrotek_framework.pdf (.386), contract_nordlicht_supply.pdf (.351)AIC_HR limit 3 threshold .3 -> ok contract_ferrotek_framework.pdf (.386), contract_nordlicht_supply.pdf (.329)AIC_HR limit 20 threshold .3 -> ok contract_ferrotek_framework.pdf (.386), contract_nordlicht_supply.pdf (.329)At every limit, AIC_HR gets passages of the contracts that the HR user can see, also at p_limit 1. The two Helios chunks rank first, and the search continues past them.
The number after a file is the average score of its chunks in the result. At p_limit 2, one chunk of the Nordlicht contract is in the result. At 3 and more, a second chunk with a lower score is in it too, so the average falls from .351 to .329.
Explain why a restricted user can get no result
Section titled “Explain why a restricted user can get no result”A restricted user gets a “nothing found” answer for a document that exists, which is the correct result for them. Part A shows this case: AIC_HR gets no_passages for the Helios question, because no contract that the HR user can see reaches the threshold of the collection.
But “nothing found” also appears when the document does not exist, or when the search did not retrieve it. The answer cannot tell these cases apart, and it must not: if it said “restricted”, it would reveal that the document exists.
The search can also miss a document that the user can see. This occurs when documents that the user cannot see fill all the candidates, up to the maximum number. A large collection where restricted documents dominate a topic can show this effect. The 14 chunks of this course cannot show it.
If restricted users miss documents that they can see, raise default_limit in the configuration of the collection. The maximum number of candidates grows with the limit, up to 500 chunks.
A higher limit also sends more chunks to prepare_sources, so the model gets more text and uses more tokens.
The tool for the model has no limit parameter. A test can set the limit through aic_rag_tools_api.search.
Run the agent as each user
Section titled “Run the agent as each user”Part C asks the same questions through the agent, as each user:
=== Part C: the same questions through the agent ===--- as AIC_LEGALQ: What notice period applies to the Helios distribution agreement?A: The notice period is **twelve months to the end of a calendar year**: “Either party may terminate this Agreement with a notice period of twelve months to the end of a calendar year.” (contract_helios_distribution.pdf) tool calls 1, tokens 1420 in / 83 out, 5.3 s
--- as AIC_HRQ: What notice period applies to the Helios distribution agreement?A: I found nothing about the Helios distribution agreement in the documents I can see, so I can’t confirm its notice period. tool calls 2, tokens 1925 in / 89 out, 6.4 s
--- as AIC_HRQ: Which salary band is a senior engineer in?A: A senior engineer is in **band B4**: “A senior engineer is placed in band B4.” (salary_bands.pdf) tool calls 1, tokens 1334 in / 63 out, 3.6 s
--- as AIC_LEGALQ: Which salary band is a senior engineer in?A: I found nothing in the documents I can see that identifies the salary band for a senior engineer. tool calls 2, tokens 1919 in / 84 out, 6.1 sThe restricted user’s agent makes a second search with other words, and then says that it found nothing in the documents it can see. That costs 500 to 600 more input tokens than the answer for the user who may read the document.
Read the trace of the last run
Section titled “Read the trace of the last run”The answer “I found nothing” does not show what the agent searched for. UC AI records every run in two tables. uc_ai_agent_executions has one row for each run: its status, the number of tool calls, the tokens, and who ran it.
uc_ai_agent_messages has one row for each message: the question, each tool call, each tool result and the answer. The UC AI tutorial See what the agent did explains the tables in detail.
Run @13_aic_trace.sql straight after 12_aic_two_users.sql. It calls no model. It reads the newest conversation of the agent aic_assistant in four parts. The second part lists the messages of the last run:
select m.seq , m.role , m.tool_name , case m.role when 'tool_call' then to_char(substr(m.tool_input, 1, 110)) when 'tool_result' then 'status ' || json_value(m.tool_output, '$.status') || ', ' || length(m.tool_output) || ' characters' else to_char(replace(substr(m.content, 1, 110), chr(10), ' ')) end as what from uc_ai_agent_messages m where m.execution_id = (select max(id) from uc_ai_agent_executions where agent_id = (select id from uc_ai_agents where code = 'aic_assistant')) order by m.seq;=== 1. The runs of the latest conversation: tokens, tool calls, who ran them ===
EXECUTION_ID STATUS TOOL_CALLS_COUNT TOKENS_IN TOKENS_OUT SECONDS CREATED_BY AUDIENCE_______________ ____________ ___________________ ____________ _____________ __________ _____________ ________________ 553 completed 2 1919 84 6.1 AIC_LEGAL authenticated
=== 2. The messages of the last run, in order ===
SEQ ROLE TOOL_NAME WHAT______ ______________ ______________________ ____________________________________________________________________________________________________ 1 user Which salary band is a senior engineer in? 2 tool_call AIC_SEARCH_POLICIES {"max_sources":5,"query":"senior engineer salary band"} 3 tool_result AIC_SEARCH_POLICIES status no_passages, 212 characters 4 tool_call AIC_SEARCH_POLICIES {"max_sources":5,"query":"salary bands engineering senior"} 5 tool_result AIC_SEARCH_POLICIES status no_passages, 212 characters 6 assistant I found nothing in the documents I can see that identifies the salary band for a senior engineer.
=== 3. What a tool result held: the passages the model saw (the latest run that found some) ===
DOCUMENT SCORE MATCHED_CHUNKS TEXT___________________ ________ _________________ _________________________________________________________________________salary_bands.pdf 0.651 1 salary_bands.pdf CONFIDENTIAL - HR only Salary Bands 2026 Gross ye
=== 4. tool_status against the real status in tool_output, over every run of the agent ===
TOOL_STATUS RESULT_STATUS N______________ ________________ ____success ok 8success no_passages 8success no_identity 1The newest run is the last run of Part C: the Legal user asked for the salary band, with the same 1,919 input tokens. The second part shows the words that the model chose for each search, and that both searches returned no_passages.
The third part shows the passages of the latest search that found some, so you can see exactly what the model read.
The fourth part compares two statuses. UC AI writes success into tool_status for every tool result, also for a search that found nothing and for a missing identity.
Read the status inside tool_output instead. The row no_identity is the run of 09_aic_identity_wipe.sql in lesson 2. Your counts include every run of the agent in your schema, so they differ from the recording.
After you read the trace, you can ask the same questions in the chat page of lesson 4: sign in as AIC_HR and ask for the Helios notice period. Each chat run adds a row to the trace tables, so run 13_aic_trace.sql before you use the chat page if you want the output of the recording.