2 · Require roles and confirmation
Tutorial step 2: restrict actions to roles and ask the user to confirm before the agent changes data, then try both cases.
In this tutorial, you'll:
- Restrict the actions that change data with
RequiredRoles - Keep a confirmation on every change with
RequiresConfirmation - Sign in as a user with the role and as one without, and compare
- Tell what the model receives when a call is refused
On this page
Right now anyone who reaches the widget can add a note. In this step you decide who may change a case. The app gets two roles for its support staff: Agents may add notes, and Supervisors may also close cases. Here Agents is a role of people, not an AI agent. Every change still asks for confirmation.
Before you start: finish 1 · Turn service methods into actions.
Put rules on the actions that change data
Section titled “Put rules on the actions that change data”Replace the content of CaseService.cs. The reads stay as they are; AddNote gets roles, and a new action closes a case:
using System.ComponentModel;
using Novior.MentorAgent.Attributes;
public class CaseService(CaseStore store)
{
[MentorAction(
Description = "Gets a support case by its number, with its status, priority and notes",
ReadOnly = true)]
public SupportCase? GetCase(
[Description("The case number, for example C-1")] string caseNumber)
=> store.Find(caseNumber);
[MentorAction(
Description = "Lists support cases, optionally only those with a given status",
ReadOnly = true)]
public IReadOnlyList<SupportCase> ListCases(
[Description("Open, Waiting for customer or Closed. Leave empty for every case")] string? status = null)
=> store.List(status);
[MentorAction(
Description = "Adds a note to a support case",
RequiresConfirmation = true,
RequiredRoles = ["Agents", "Supervisors"])]
public string AddNote(string caseNumber, string text)
{
store.AddNote(caseNumber, text);
return $"Note added to case {caseNumber}.";
}
[MentorAction(
Description = "Closes a support case with a resolution",
RequiresConfirmation = true,
RequiredRoles = ["Supervisors"])]
public string CloseCase(string caseNumber, string resolution)
{
store.Close(caseNumber, resolution);
return $"Case {caseNumber} closed.";
}
}RequiredRoleslists the roles that may run the action. Holding any one of them is enough:AddNoteis for agents and supervisors,CloseCaseonly for supervisors.RequiresConfirmation = truekeeps the confirmation box on both. The role decides who may act. The confirmation asks the user, every time, whether to act. A[MentorAction]that is not read-only asks even without it. WritingRequiresConfirmation = trueputs the rule in your code.CloseCasebecomes the toolclose_case.- The reads have no rules, so anyone who reaches the widget can call them. Protect reads the same way when the data is not for everyone.
Sign users in
Section titled “Sign users in”MentorAgent checks roles with ClaimsPrincipal.IsInRole (the ClaimTypes.Role claims, by default) on the signed-in user. It gets that user from the AuthenticationStateProvider that Blazor already has. The Cases app has no sign-in yet. In a real app, users sign in with ASP.NET Core Identity, Microsoft Entra ID or another scheme. For this tutorial, a cookie and a test endpoint are enough.
At the top of
Program.cs, add theusingdirectives:using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies;Before
builder.Build(), register cookie authentication:builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie();After
var app = builder.Build();, add the test endpoints:if (app.Environment.IsDevelopment()) { // For this tutorial only: sign in as a test user, with or without a role app.MapGet("/dev/sign-in", async (HttpContext http, string name, string? role) => { List<Claim> claims = [new(ClaimTypes.Name, name)]; if (!string.IsNullOrEmpty(role)) claims.Add(new(ClaimTypes.Role, role)); await http.SignInAsync(new ClaimsPrincipal( new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme))); return Results.Redirect("/"); }); app.MapGet("/dev/sign-out", async (HttpContext http) => { await http.SignOutAsync(); return Results.Redirect("/"); }); }
/dev/sign-in?name=Alex&role=Agentssigns you in as Alex, with the roleAgents. Leave outroleto sign in with no role, and open/dev/sign-outto sign out.- ASP.NET Core adds the authentication middleware by itself when authentication is registered, so the rest of the pipeline stays as it is.
- Each endpoint redirects to the home page. The page loads again, and the widget works for the new user.
Try it
Section titled “Try it”Run the app and try the same requests as three different users. To sign in as a user, add a path such as /dev/sign-in?name=Alex&role=Agents to the address of the app in the browser.
Without signing in
Section titled “Without signing in”- Type "Close case C-2, it is a duplicate of C-1". No confirmation box appears. The agent tells you that you need to sign in, and the case stays open.
With the Agents role
Section titled “With the Agents role”- Open
/dev/sign-in?name=Alex&role=Agents. - Type "Add a note to C-2: same issue as C-1". The confirmation box appears. Click Confirm, and the note is added.
- Type "Close case C-2, it is a duplicate of C-1". No box appears: the agent tells you that closing a case needs the role Supervisors.
With the Supervisors role
Section titled “With the Supervisors role”- Open
/dev/sign-in?name=Sam&role=Supervisors. - Ask again to close C-2. The confirmation box shows the case number and the resolution.
- Click Cancel. The method does not run, and the agent tells you in one sentence that it did not proceed.
- Ask once more and click Confirm. Case C-2 is closed.
What the model receives
Section titled “What the model receives”- The same tools for everyone.
close_caseis in the tool list even when Alex is signed in. The check runs in your app when the tool is called, not in the model. - Who is asking. For a signed-in user, the model also receives the name and the roles. For a user who is not signed in, it receives nothing about the user.
- A refusal instead of a result. When the role is missing, the method does not run and no box appears. The model receives an access-denied error instead. The error names the required roles, or says that the user must sign in, and the model passes it on to the user.
- A cancellation. When the user clicks Cancel, the method does not run. The model is told that the user declined and that nothing was checked or changed. It is asked not to try again, not to give another reason, and not to describe the state of any record.
The order never changes: first the role, then the confirmation, then your method. MentorAgent reads the roles once more after the user confirms, so a user who lost the role while the box was open is refused.
Next step
Section titled “Next step”The coordinator, the main agent that answers you, does everything itself. In 3 · Hand off to a specialist agent you move the case actions to a specialist, and the rules move with them.
Two checks, in this order explains both checks, and Where nobody can answer what changes on the other channels.
What you learned
Section titled “What you learned”- Restrict the actions that change data with
RequiredRoles - Keep a confirmation on every change with
RequiresConfirmation - Sign in as a user with the role and as one without, and compare
- Tell what the model receives when a call is refused
See also
Section titled “See also”- Add MentorAgent to an existing app — Add MentorAgent to a Blazor Web App you already have: install the package, register the services, mark your methods and place the widget.
- Attributes — The parameters of MentorAction, MentorAgent, MentorTeam, TeamMember, MentorPage and MentorSkill, with their defaults and rules.
