The public endpoint that returns too much
A customer portal shares a module, Customers, with a partner integration. GetCustomerById was created by setting Expose as REST API on an existing service action, authentication left at the default, and it returns the Customer entity itself: tax id, IBAN and an internal credit score. The logistics partner calls it roughly 300,000 times a day. The module also has a screen /health with Authentication set to No, which returns the count of open tickets read from a private entity - it works, so nobody questioned it. Three roles exist: PortalUser, Agent, Admin. Two client actions in the portal invoke the public server action DeleteTicket; the only authorisation check anywhere is a CheckRole for Agent in the screen's On Ready. A Timer named SyncInvoices has its Username property set to an employee whose account was deactivated last week; it has failed every night since.
1. Rework the partner API for least privilege and describe what you change in the response shape and the method configuration.
Read the worked answer
Return a contract, not an entity. Returning the Customer entity means every column added later silently becomes part of a public contract, and internal fields such as the credit score and tax id were never intended to leave. Define an output structure with the six or seven fields the partner actually consumes, and map into it explicitly. Data exposure then becomes a decision made in code rather than a side effect of a model change.
Authenticate per method and scope per caller. Set authentication on the exposed method (Basic or a custom token scheme, per your platform capability) rather than leaving the default, terminate TLS, and identify the caller so the query is filtered to customers that caller is entitled to see. Authentication without row-level scoping means any authenticated partner can enumerate the customer base by incrementing the id - the data leak is authorisation, not secrecy.
Add the operational controls the volume demands: rate limiting and throttling at the gateway, request logging with the caller identity, and alerting on 401/403 spikes, because a partner silently falling back to a broken credential pattern is exactly the failure you will otherwise discover from the invoice.
Keep the old shape until the partner migrates. Add the new versioned method alongside, let them switch, then retire the original by removing the Expose as REST API setting - and confirm no other consumer appears in the traffic logs first, since exposure settings rarely have exactly one caller.
2. Where must authorisation be enforced, and why is the existing check insufficient?
Read the worked answer
Client-side checks are user-interface hints, not security. On Ready runs in the browser. A client action can be invoked from a browser console or by replaying the request, and DeleteTicket is a public server action, so it is reachable as a server call regardless of what the screen did. The check must live inside the server action, evaluated on every call, before any write.
Layered enforcement:
- the server action
DeleteTicketstarts with a role check forAgentand returns an explicit error when it fails - the caller cannot skip it because it is in the same request path as the delete; - the screen keeps its Role property set for defence in depth and to shape what users see, which is a usability concern rather than a control;
- the platform's generated entity role checks are applied where entities are exposed, so that a future action that forgets to check still fails;
- the delete itself is written so that a repeated call is harmless and logged with the acting user, which matters when the audit asks who removed a ticket.
The anonymous screen. It succeeded because OutSystems does not filter rows by role at the database level - the aggregate ran server-side with the module's own access. Serving a count from a private entity to an unauthenticated caller is a small but real disclosure: it confirms the entity exists and leaks business volume. Replace it with a dedicated server action that returns a single health value with no entity access, or move health checking to the monitoring tool where it belongs. 'It works' is not an access-control review.
3. How should scheduled work be run so that it does not depend on a person's account?
Read the worked answer
A timer runs as an identity, and that identity must be an owned service account. SyncInvoices stopped because the Username property pointed at a leaver's deactivated account. The nightly failure is the visible symptom; the real defect is that a production process had a personal dependency, so any offboarding, password rotation or role change could take it down silently.
What to do:
- create a dedicated integration account owned by a team mailbox, not an individual;
- grant it only the roles the timer's actions need - an
Integrationrole, neverAdminand neverPortalUser, because a timer with administrator rights turns any bug in the sync into a privileged data corruption event; - repoint the timer's Username property, verify the next run succeeds, and confirm entity-level access is sufficient for every action the timer calls, not just the first one;
- add alerting on timer failure and on 'no successful run in 24 hours', so the next breakage is detected the same night rather than by a customer.
Also check the blast radius of the outage: the invoices that failed to sync for a week need a controlled backfill, and the backfill should be idempotent so that re-running it after a partial failure cannot double-post. Use the same service identity, and record the run and its outcome so the next person can tell what was processed.