# API Maker Documentation : Security > The pages of https://docs.apimaker.dev in the order of the navigation, each with its title, its description and its URL. The code is in TypeScript : the Markdown of a page (its URL with .md) has every language. --- # Security Overview > How API Maker keeps your data safe - every API locked by default, the API gate and the row gate, what is enforced where, the trust model of the sandboxes, what the licence check sends, and the checklist before you go live. Source: https://docs.apimaker.dev/v4/docs/security/overview.html Every API of API Maker is locked until you open it, and two gates decide who reaches what : groups decide **which APIs**, a pre hook decides **which rows**. The [security report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) reads your settings and your code, and says where the second gate is missing. ## Locked by default | Part | How it is locked | |---|---| | Database and custom APIs | A call needs the token of an [API user](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) whose groups grant that API. Only the APIs you mark `IS_PUBLIC` in their [settings](https://docs.apimaker.dev/v4/docs/settings/apiSettings.html) skip it. | | System APIs | Closed over HTTP : your code calls them, an HTTP call answers `401` until a [system API setting](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) opens it. The token API is the exception. | | Fields | A group grants read and write per field : a field it does not grant is absent from the answer. | | Secrets | Keys and connection strings live in the [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.html), encrypted on the server and never in Git. Each account gets keys of its own. | A new account starts open for its first steps : an API user `default` with the password `12345`, and a group `Default` which grants everything. Replace both before real data arrives : see [Before you go live](https://docs.apimaker.dev/v4/docs/security/go-live.html). ## Two gates ![The API gate and the row gate](https://docs.apimaker.dev/assets/images/role-based-permissions/rbac-two-gates.svg) - **The API gate : who may call which API.** The application sends the token of its API user (`x-am-authorization`) ; its [groups](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) grant APIs, tables and fields. A person sends a token of their own (`x-am-user-authorization`), made by an [auth provider](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) ; their groups must grant the API too. - **The row gate : which rows that person reaches.** Groups can not know that order 1042 belongs to Ada : that is a fact of the data. A [pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html#narrow-a-request-to-the-rows-of-the-caller) adds the owner to every filter and stamps it on every write. The security report installs it in one click. > Diagram : The row gate : the same call, without and with the row scoping pre hook > > Without a row scoping pre hook, Diya calling get all on orders gets every order of the table, hers and Kabir's. With the hook, API Maker adds her customer id to the filter with $and before the query runs, and she gets only her two orders. [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html) builds both gates step by step, and tries to break them. ## What is checked where | Question | Answered by | Page | |---|---|---| | Is the caller an application we know ? | The token of an API user | [API users](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) | | Which person is it ? | The token of an auth provider | [Auth providers](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) | | May they call this API, read this field ? | Groups | [Groups](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) | | Is this row theirs ? | A pre hook | [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) | | Is this data valid ? | The schema of the table | [Schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) | | Where is a leak ? | The security report | [Security report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) | | From which web sites ? | Allowed origins | [Security features](https://docs.apimaker.dev/v4/docs/features/security-features.html#allowed-origins) | ## The sandboxes : what they protect, and what they do not The code of an account (custom APIs, hooks, schedulers, events) runs in Docker containers of that account, apart from the process of API Maker, with a [time limit](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html#time-of-a-run). They keep API Maker running when code fails : an exception ends its request with an error, and code which runs too long is cut short while a new runner takes the next requests. > Diagram : Where code runs : sandboxes per account and language, and the native process > > One API Maker process per server hands the code of each account to the sandboxes of that account : Docker containers, one kind per language, more of them while they are busy, up to the sandbox count. Code marked runOnNativeProcess runs on the machine instead, beside API Maker. The containers run privileged and without a memory limit : they guard against faulty code, not against hostile code. They are not a wall against hostile code : - **The containers run privileged.** Code which wants to can reach the server around them, and from there the secrets of every account of the server. - **They have no memory limit.** A leak takes memory of the whole server. - **The native process is not isolated at all.** Code with `runOnNativeProcess: true` runs on the machine of the server, beside API Maker. So run on one server only code you trust : your team's, or an AI's that your team reviewed. **For clients who must not see each other's data, use separate servers**, not separate accounts of one server. ## What API Maker sends to us API Maker has no analytics. A server checks its licence key with the licence server of API Maker (`sava.be.apimaker.dev`), and sends nothing else : | | | |---|---| | What it sends | The licence key, the MAC addresses of the machine and its number of CPU cores. | | When | A minute or two after it starts, every day at about 12:00 (time of India), every four hours while a check gets no answer, and when a key is written. | | What it never sends | Data of your databases, your APIs, logs, code or users. | | What a failed check does | Nothing to your APIs : the licence page of the admin panel shows the problem. A licence server which does not answer changes nothing. | | The key of API Maker Launchpad | Never sent : the licence page shows `LOCAL`. | ## Before you go live The [checklist](https://docs.apimaker.dev/v4/docs/security/go-live.html) : default passwords, the default group, the row gate, public APIs, HTTPS, backups. ## Report a vulnerability Write to [contact@apimaker.dev](mailto:contact@apimaker.dev) with the steps to reproduce it. Please give us time to fix it before you publish. --- # Before You Go Live > The checklist before an API Maker server takes real users - default passwords, the default API user and group, the row gate, public APIs, HTTPS, the licence key, backups and the sandboxes. Source: https://docs.apimaker.dev/v4/docs/security/go-live.html A new installation is set up for its first steps, not for real users. Go through this list before real data arrives, and again after a big change. ## Accounts and passwords - [ ] **Change the passwords of the admin panel.** The install script creates `root@root.com` (`R00t_123456789`) and `admin@admin.com` (`Admin_123456789`) : change the first in **Root Settings**, the second in its profile. - [ ] **Turn on [two-factor sign-in](https://docs.apimaker.dev/v4/docs/features/security-features.html#two-factor-sign-in)** for the people of the admin panel. - [ ] **Replace the API user `default`.** Its password is `12345`, in the secret (`common.apiUserPasswords.default`). Give each application an [API user](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) of its own with a long password, and delete `default` or change its password. ## Who reaches what - [ ] **Replace the group `Default`.** It grants everything. Make small [groups](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html), one per screen or per job, with the tables, APIs and fields each needs, and no `allCollections` or `allSystemApis`. - [ ] **Close the row gate.** Every table people reach through the generated APIs needs a [row scoping pre hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html#narrow-a-request-to-the-rows-of-the-caller) : the owner in every filter, stamped on every write. - [ ] **Run the [security report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html)** and fix or skip, with a reason, every finding. Aim for grade A. - [ ] **Review what is public.** The APIs marked `IS_PUBLIC`, and the [system APIs](https://docs.apimaker.dev/v4/docs/settings/systemApiSettings.html) opened over HTTP : each one has a reason. - [ ] **List the [allowed origins](https://docs.apimaker.dev/v4/docs/features/security-features.html#allowed-origins)** of your web apps, so browsers on other sites are refused. ## The server - [ ] **HTTPS only.** Install with Caddy and SSL (`--ssl=yes` and your domains, see [Install on a server](https://docs.apimaker.dev/v4/docs/getting-started/install-on-server.html)), so tokens never travel in clear. - [ ] **Set your licence key.** The key of API Maker Launchpad is for development on a computer. - [ ] **Keep the databases private.** Your databases, and the MongoDB and Redis of API Maker, answer on a private network or on the server itself, never on the internet. - [ ] **Back up.** The MongoDB of API Maker (`mongodump`), your own databases, and `/root/config/.env` with `license.txt` : a backup you have restored once is the one you can trust. ## Your code - [ ] **Run only code you trust on the server.** The sandboxes stop faulty code, not hostile code : read [what they protect](https://docs.apimaker.dev/v4/docs/security/overview.html#sandboxes). For clients who must not see each other's data, use separate servers. - [ ] **Keep secrets in the secret.** No key or password written in the code : the [code scan](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report-reference.html#the-code-scan) of the report finds them. - [ ] **Run the [test cases](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html)** : `npm run test` in the folder of the local client. ## Related - [Security overview](https://docs.apimaker.dev/v4/docs/security/overview.html) · [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html) · [Secrets](https://docs.apimaker.dev/v4/docs/secrets/secrets.html) --- # APIs Security Report & Actions > Scan an API Maker account for data leaks (unscoped collections, wide groups, weak auth providers, public APIs, leaks in the code itself in all eight languages), fix them with one click after seeing the change, or skip them with a reason, and keep the report in git for people and AI assistants. Source: https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html **Security › APIs Security Report & Actions** scans the account and tells you where a person could read or change data which is not theirs, then lets you fix it from the same page. It reads your settings (groups, API users, auth providers, collection settings) and your code : the hooks, the custom APIs, the schedulers, the events and every other piece of code of the account, in each of the eight languages API Maker runs (TypeScript / JavaScript, Python, PHP, C#, Go, Rust, Java and Kotlin). > Diagram : Scan, finding, one click, scan again : the loop of the security report > > The security report in four steps : it scans the settings and the code of the account, shows each finding with the request which proves it, fixes it in one click after showing the change, and scans again to update the score out of 100 and the grade. ## Quick start 1. **Open the report** **Security › APIs Security Report & Actions**, then **Scan**. The account gets a score out of 100 and a grade, and the overview shows where the risks are. 2. **Read the findings** Click **Review**, a severity, a category or a finding : the list opens, the worst first. Each finding says what a person could reach, and shows the request which proves it. 3. **Fix or skip** Most findings have an action : see the change, then apply it. A finding you accept is skipped, with a reason kept in git. 4. **Keep it fresh** The account is scanned again after every action, and on its own while a local client is connected. The report is in the repository for your team and your AI assistant. ## Two gates, one report Every request carries two identities, and the report checks both : | Gate | Header | Question | What the report checks | |---|---|---|---| | API gate | `x-am-authorization` | May this application call this API ? | Groups with broad flags (`allInstances`, `allCollections`, `allSystemApis` ...), sensitive system APIs, bulk update / delete grants, unused groups | | Row gate | `x-am-user-authorization`, `x-aws-authorization`, `x-google-authorization`, `x-azure-authorization`, `x-custom-authorization` | Of the rows this API reaches, which ones belong to *this person* ? | For every collection the groups reach : whether an **active pre-hook** reads the person (`g.req.auth.authAMDB` ...) and limits **every family of APIs** (`query.find` for get-all and the *-by-id APIs, `body.find` for query, count, update-many, remove-by-query ...) to their rows, merges with `$and`, stamps the owner on writes and refuses aggregate / distinct | The row gate is the point of the feature. The generated APIs are powerful : with `find`, `deep`, `find-join`, `query`, `update-many` and `remove-by-query` a caller who is allowed on a collection reaches every row of it unless a pre-hook narrows the request. The report reads the code of your pre-hooks (instance, database, collection and API level) and decides, per collection and per API, whether that narrowing happens. It also reviews the **auth providers** (unhashed passwords, tokens of Google or Azure taken whatever application they were made for, no groups column, long lived tokens) and the **public APIs** (instance APIs, custom APIs and system APIs with `IS_PUBLIC`). ### A simple example Diya and Kabir are customers of a shop. Its app calls `GET /api/schema/shop/mysql/sales/orders` with the token of the app (`x-am-authorization`) and the token of the person who signed in (`x-am-user-authorization`). - **Without a row scoping hook**, Diya gets every order of the table : hers, Kabir's, everybody's. The groups say the app may call the API ; nothing says which rows are hers. The report finds this : *"orders" is not scoped to the rows of the caller* (critical). - **With the row scoping hook**, API Maker adds her id to every filter before the query runs : `{ $and: [ , { customer_id: 2 } ] }`. Diya gets her two orders, and nothing she sends can widen that. ## What it checks Each check, with what it looks for and how to fix it, is in the [reference](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report-reference.html) : [how a hook is read](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report-reference.html#how-a-hook-is-read), [what the report finds](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report-reference.html#what-the-report-finds), [every language](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report-reference.html#every-language) and [the code scan](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report-reference.html#the-code-scan). ## The page The page has two views, **Overview** and **Findings**, side by side in its header. - **Overview** is where the page opens, in one narrow column in the middle of the page. It shows the score and the grade with the open findings by severity, one line per question of the scan (row gate, public access, code, API gate, auth providers, hygiene) with its open findings, the five findings to fix first, then what was scanned and the automatic scans on one line each. - **Findings** lists the findings on the left, grouped by category, the worst category first, with the selected finding on the right. The summary above the list is folded to one line : the score, the grade and the open findings by severity, which filter the list. Click it to see what was scanned and the automatic scans. A severity, a question or a finding of the overview opens the findings on it. A link to a finding opens the findings view on that finding. ## The score Every open finding takes points off 100 (critical 20, high 10, medium 4, low 1). Skipped and fixed findings take nothing. The grade (A to F) and the trend over the last scans are shown on the overview and printed in the report. ## Actions Each finding offers what fixes it : an action when the fix can be automated, otherwise a link to the page where the change is made. Nothing is changed behind your back : an action runs when you click it, goes through the same routes as the admin panel (validation, TypeScript compile, git sync), and the account is scanned again right after so the report shows the result. | Action | What it does | |---|---| | Add a row scoping pre-hook | Installs a collection (or database) level pre-hook which merges `{ ownerColumn: personId }` into `query.find` / `body.find` with `$and`, stamps the owner on every write and refuses aggregate / distinct. The dialog has two tabs. On **Settings** you pick the owner column and the field of the token, name the hook, choose its level and can limit it to some groups of API users (the groups which reach the collection are listed first, with their API users). On **Code** you pick the language (see [Every language](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report-reference.html#every-language)) and can edit the code before it is saved : what the report reads in it is shown as you type, and on the tab itself. The help of each field is behind its help icon. | | Rewrite the hook as a row scoping hook | Replaces the code of a hook of the collection which scopes only part of the APIs, lets the owner be overridden or bypasses the scoping, by the row scoping hook, in the language of the hook. The hook keeps its name, its groups and its place. The owner column, the field of the token and the three behaviours sit on one band above the code. You see its code now and after, side by side, and can change the new code before it is saved. | | Let the internal calls through | Adds the guard of the internal calls (`if (!g.req.isApiRequestFromUser) return;` in TypeScript, the same in the language of the hook) at the top of its `main` : custom APIs, schedulers and events which call `g.sys.db` are not scoped to a person they do not have. Nothing else of the code changes. | | Run the hook for these groups too / for every group | Adds the groups which reach the collection without being scoped to the `groupNames` of the hook, or clears them so the hook runs for every caller. | | Require a token | Turns `apiAccessType` from `IS_PUBLIC` to `TOKEN_ACCESS` in the settings of a collection, in the Basic Info of a custom API, or in the settings of a system API. Only that word of the code changes. | | Apply the fix (code scan) | Makes the change of code a finding of the code scan proposes (the checks of TLS turned back on, the password column left out of the rows read ...), where the code is kept : in the version in use of a custom API, a scheduler or a listener with versions, in the hook of a version of a custom API. | | Add "column" to the token of a provider | Puts a column of the users table into the `select` of a DB token generator, so the token carries it and the hook can compare it. Persons log in again to get the new token. | | Require a person token on this collection | Writes collection settings naming the auth providers, so every API of the collection needs a person token next to the API user token. Settings which keep the collection closed (`NO_ACCESS`) keep it closed. | | Activate the hook | Switches an inactive row scoping hook back on. | | Freeze to an explicit allow-list / turn the flag off | Narrows a broad group flag. Freeze writes what the group reaches today as explicit grants first, so nothing added later is reachable through it. The dialog shows the flag and the two ways side by side : a flag which can not be written as grants (fields, system APIs, custom APIs, WebSocket events) can only be turned off. | | Revoke grants | Takes sensitive system APIs, bulk APIs, or the APIs which write the users table, away from a group. The dialog lists them all checked : uncheck the ones the group keeps. | | Skip | Marks a finding as acceptable, with a reason (type it, or pick a quick reason). It leaves the score, and the reason is kept in git. | A finding which suggests code shows it in a read only editor, in its language, and links such as "Open the collection", "Open the group" or "Open the code" open the page of the target in a new tab, so the report stays where it is. ### See the change first An action which changes code shows the change before anything is saved : the new hook, or the code now and after the change side by side, with what the report reads in a hook after it (whether it scopes rows, which families of APIs it covers, whether internal calls go through). Code which would not parse is not saved. The code is then saved through the route of its own page, as if you saved it there : it is compiled and checked the same way (one language per API included), it goes to git and to the local client, and the account is scanned again. A fix is made only on the code the scan read : when the code changed since, the action says so and asks for a new scan. ## Automatic scans While a **local client** is connected to the account, API Maker scans it on its own and keeps the report fresh, on the disk of the developer and in git, so an AI assistant working in the folder of the project always reads the current report. - **The pace follows the account.** The next scan comes 100 times the duration of the last one later, never closer than 1 minute and never further than 30 minutes : a scan which takes 300 ms runs every minute, one which takes 20 s runs every 30 minutes. - **Nothing runs when nothing changed.** Between two scans, only a fingerprint of the inputs is read (one count and one date per collection of API Maker : groups, API users, auth providers, hooks, settings of tables and of system APIs, schemas, instances, custom APIs, every other piece of code the code scan reads, the secrets and the admin user). When it is the one of the last scan, no scan runs. With the live catalog on, a scan still runs every 30 minutes, for the tables added to a live database. - **Nothing is saved when the report did not change.** A scan which finds the same findings saves nothing, so the local client and git see nothing. A scan which finds a different report saves it, sends the folder to the local client and, when the account has a git repository, commits the folder `src/api_security_report/` alone (what you have not committed yet stays as it is), at most once every 5 minutes. - **One at a time.** A scan runs in the process which holds the connection, one at a time per account. The panel **Automatic scans** (on the overview, and in the summary above the findings) shows whether a local client is connected, what the last cycle did, when the next one is due at the earliest, and the last commit. Two switches, saved with the report, turn the automatic scans and the automatic commits off ; the play button runs one cycle right away. ## In the repository The report is committed with the rest of the project as the folder `src/api_security_report/` : | File | Content | |---|---| | `README.md` | What the folder is, for people and AI assistants : read only, work on the findings, rescan | | `report.md` | The full report : summary, scope, every finding with its evidence, the request which exploits it, what to do, and how to fix it : every fix of the page written as the change of the repository it makes | | `report.yaml` | The same, machine readable | | `actions.yaml` | The skipped findings with their reasons, and the log of the actions taken from the page | API Maker only ever **writes** these files. The one thing it reads back is the `skipped` section of `actions.yaml`, on a git pull, so the decisions of the team follow the project. An AI assistant working on the repository can read `report.md`, fix the hooks, groups, settings and providers in their own folders, commit, and the automatic scan (or you, from the admin panel) rescans. ### How to fix it, in the files Under **How to fix it**, every finding lists the fixes its page offers, and writes each one as the change of the repository it makes : the files to create (the YAML and the code of a new hook, new settings), the lines of a file which change, the keys of a YAML file to set, the values to add to the lists of a group or to take out of them. Made in these files (by you or by an assistant), committed, pushed and pulled from the Git page (or saved while the local client runs), the change fixes the finding exactly as the button does : the same code, in the language of the hooks of the table, the same names and folders, the same grants. `report.yaml` holds the same changes as data (`actions[].repository`). ```md **Fix 1 : Revoke these system APIs** (recommended) In `src/groups/app/app.yaml` : - remove from `queryParams.systemApiNames` : `EXECUTE_PLAIN_QUERY` ``` A fix which needs a choice first (the column which says who owns a row, when the report can not tell) says so, and the change of the default secret (`common.authProviders`) is made on the Secrets page : the secrets are not in the repository. The [`AGENTS.md`](https://docs.apimaker.dev/v4/docs/ai/llms.html#agents-md) API Maker writes at the root of the repository tells assistants how : read the finding, change the file it names, let the next scan show it fixed, and leave skipping to your team. ## Printing The download menu of the page prints the report (A4, with or without the skipped findings) through the print dialog of the browser, where "Save as PDF" makes the file, and downloads `report.md`, `report.yaml` or `report.json`. ## Related reading - [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html) - [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) - [Authorization of AM database user](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) - [Pre-hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) --- # Security Report Reference > Every check of the APIs Security Report of API Maker - how a pre hook is read in every language, what the report finds in groups, auth providers and public APIs, and the rules of the code scan with their severity and fix. Source: https://docs.apimaker.dev/v4/docs/apis-security/api-security-report-reference.html Every check of the [APIs Security Report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html), with what it looks for and how to fix it. ## How a hook is read The report reads the code of a hook, it never runs it, and it judges what the code does, not its name or its comments. A hook scopes rows when the **person behind the request reaches the filter** : ```typescript // Scopes : the id of the person goes into the filter. const me = g.req.auth.authAMDB.customer_id; g.req.query.find = { $and: [g.req.query.find || {}, { customer_id: me }] }; // Does not scope : it reads the person to decide something, then writes a filter of its own. // Every person still reads the orders of every other person (the active ones). if (g.req.auth.authAMDB.role !== 'admin') { g.req.query.find = { $and: [g.req.query.find || {}, { status: 'active' }] }; } ``` The report follows the id of the person through variables (`me`, `scope`), objects and helper functions, in all eight languages. A few more things decide what counts : - A hook runs when it is active (`isActive` true) ; one which does not say so does not run, and does not count. - A hook of an API (API level) runs before that API only : a hook of *get all* does not scope *get by id*, and the report lists the APIs it leaves open. - A hook which picks APIs by their id scopes the ones it picks : `if (apiId === 'SCHEMA_GET_ALL') { … }` leaves *get by id*, *update by id* … with the filter their caller sends, and the report lists them. A `switch` with a `default: throw`, or a refusal after the branches, refuses the APIs it does not handle : nothing is open there. TypeScript is read with its `if`s, `switch`es and early `return`s ; the other languages with their `if`s. - The person has to keep the rows to their own : a filter which gives the person as one choice of an `$or` beside the filter of the caller (`{ $or: [find, { customer_id: me }] }`, any caller sends a filter every row matches), or puts the person under `$ne`, `$nin`, `$not`, `$gt` ..., lets the rows of everybody else through. The report reads it as no scoping and says why. - An API whose settings say `NO_ACCESS` answers nobody outside the admin panel : it is not looked at. A public one (`IS_PUBLIC`) asks for no token at all : its own finding says so. The access of an API is the one its settings say, else its collection's, else its database's. - A group whose `active` is false still grants : a request reads the groups of its API user by their names alone. The report counts its grants, and says it is switched off. `deep`, `find-join` and master save reach nothing more : for a request of a person or an application, the rows of the related collection are read (and, for master save, changed) through its own APIs as that caller : with the group check of the caller, the pre and post hooks of that collection (its row scoping), and never in a collection whose settings say `NO_ACCESS`. Code which calls `g.sys.db` runs under its own rules : the hooks run when it passes `skipHookRunning: false`. A `find` carries no SQL of its own either : on the SQL instances every value of a query is a parameter of the statement. ## What the report finds Besides the code (see [The code scan](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report-reference.html#the-code-scan)), these are the findings of the settings, by category : | Finding | Severity | What it means | |---|---|---| | **Row gate** | | | | A collection not scoped to the caller (`ROW_SCOPE_MISSING`) | critical when its owner column is sure, high or medium otherwise | No active pre-hook puts the person into the filter : every person reads every row | | The users table reachable (`USERS_TABLE_EXPOSED`) | critical | The users table of an auth provider is reachable by the groups and not limited to the row of each person : every account can be listed | | A person can change their own account (`USERS_TABLE_WRITABLE`) | high, medium without a groups column | The users table is limited to the row of each person, but the groups may write it : a person changes their own groups (`"*"` is every group) or password | | Rows of persons without a person token (`ROW_SCOPE_NO_PERSON_TOKEN`) | high or medium | The rows have an owner, but the APIs ask for no person token : nothing can scope them | | No person token anywhere (`NO_PERSON_TOKEN_ANYWHERE`) | high | No API of the account asks for the token of a person | | The scoping hook is off (`ROW_SCOPE_HOOK_INACTIVE`) | high | The hook which would scope the rows is switched off | | Some APIs not scoped (`ROW_SCOPE_PARTIAL`) | high | The hook scopes some families of APIs, or some APIs (a hook of one API), and not the others | | Aggregate / distinct bypass the scoping (`ROW_SCOPE_BYPASS_APIS`) | high | They have no place for the filter of the owner, and nothing refuses them | | Some groups not scoped (`ROW_SCOPE_GROUP_GATED`) | high | The hook runs for some groups only (`groupNames`), other groups reach the collection too | | A later hook undoes the scoping (`ROW_SCOPE_UNDONE_BY_LATER_HOOK`) | high | A hook which runs after the scoping one gives the filter a new value | | Writes trust the owner sent (`ROW_SCOPE_OWNER_NOT_STAMPED`) | medium | Saves keep the owner the caller sent : a person creates rows for someone else | | A weak merge (`ROW_SCOPE_WEAK_MERGE`) | medium | The owner is put next to the filter of the caller with a spread, or in its place, not with `$and` | | Internal calls scoped too (`ROW_SCOPE_NO_INTERNAL_GUARD`) | info | Custom APIs, schedulers and events which carry no person are scoped too | | **Public access** | | | | A public collection or API (`INSTANCE_API_PUBLIC`) | critical | `IS_PUBLIC` : anyone on the internet reads, writes and deletes the rows | | A public system API (`SYSTEM_API_PUBLIC`) | critical for the sensitive ones, lower for the others | `IS_PUBLIC` in the settings of a system API : anyone runs a query, reads the secrets, makes the server call an address ... (the API which gives tokens is public by design and not reported) | | A public custom API (`CUSTOM_API_PUBLIC`) | medium when it reads the databases, low otherwise | The custom API answers without a token | | **API gate** | | | | Broad flags (`GROUP_ALL_INSTANCES`, `GROUP_ALL_DATABASES`, `GROUP_ALL_COLLECTIONS`, `GROUP_ALL_INSTANCE_APIS`, `GROUP_ALL_SYSTEM_APIS`, `GROUP_ALL_CUSTOM_APIS`, `GROUP_ALL_WEB_SOCKET_EVENTS`, `GROUP_ALL_FIELDS`) | critical to low | A group reaches everything of a kind, including what is added later | | Sensitive system APIs granted (`GROUP_DANGEROUS_SYSTEM_API`) | high | Execute plain query, Get secret keys, the cache APIs, the indexes, Call external API, Is valid connection string | | Bulk update / delete granted (`GROUP_BULK_WRITE_APIS`) | medium | update-many and remove-by-query, which change every row a filter matches | | **Auth providers** | | | | Passwords stored in clear (`PROVIDER_PASSWORD_NOT_HASHED`) | critical | The schema of the users table neither hashes nor encrypts the password column | | Tokens of any application taken (`SSO_AUDIENCE_NOT_CHECKED`) | high | Google without `clientId`, Azure with an empty `audience` : a token made for another application (which any site can get from its own users) is taken as a login | | Long lived tokens (`PROVIDER_LONG_EXPIRY`) | medium | A token lives more than 30 days | | No groups column, no groups source, no condition (`PROVIDER_NO_GROUPS_COLUMN`, `SSO_NO_GROUPS_SOURCE`, `PROVIDER_NO_CONDITION`) | low | Every person gets the groups of the API user, or a disabled account still logs in | | **Hygiene** | | | | Unused groups and providers, API users without groups, Swagger docs on (`GROUP_UNUSED`, `PROVIDER_UNUSED`, `API_USER_NO_GROUPS`, `API_USER_SWAGGER_ENABLED`) | info | Loose ends which hide the real problems | A token of a DB token generator never carries the password of its person : it carries a fingerprint of it (made with the secret of the tokens, which never leaves the server), or nothing when the provider names a `passwordChangedAtColumn`, and a password which changes ends the tokens made before. There is nothing to find there. ## Every language A hook can be written in any language API Maker runs, and the report reads all of them the same way : a Python hook which sets `g.req.query["find"]`, a Go hook which calls `g.Req.Query.Set("find", ...)` and a TypeScript hook which assigns `g.req.query.find` are the same scoping to it. The checks of the row gate (which identity is read, which families of APIs are filtered, whether the owner is merged with `$and`, stamped on writes, whether aggregate and distinct are refused, whether internal calls are let through) work in every language. The fixes write code in the right language too. An API runs the code of one language on one runtime, its hooks included, so : - a new row scoping hook is written in the language of the hooks which already run for the collection, and the language can not be changed in the dialog (another one would be refused when it is saved) ; - when no hook runs for the collection yet, the language most of the code of the account is written in is proposed, and any of the eight can be chosen ; - a hook which is rewritten, or given the guard of the internal calls, stays in its own language. ## The code scan Settings tell who may call what. The code decides what an API then does, and it can leak data on its own : a custom API which returns the password column, a hook which takes the owner from the body, a query put together from what the caller sends. The scan of the code reads every piece of code of the account, in every language : - the custom APIs, and the hooks of the version in use of each, - the hooks of the instances, databases, collections and APIs, before and after, - the hooks of the system APIs, - the schedulers, the listeners of the events, the WebSocket events, the utility classes, the process initializers and the database migrations, - the functions of the auth providers. Its findings are in the category **Code**. Each one names the piece of code, the line and the language, shows the lines, says why it matters and what to do, and links to the page where the code is edited. A custom API which answers without a token makes most findings one level more severe : anyone on the internet can call it. | Finding | Severity | What it means | One click fix | |---|---|---|---| | A secret in the code (`CODE_HARDCODED_SECRET`) | high ; critical for a private key, an AWS access key, a live key of Stripe ; medium for a test key | A key, a password or a token is written in the code, so in git and every copy of it (the report shows it masked) | the finding shows the code which reads it from Secret Management, in the language of the code | | A secret returned (`CODE_SECRET_RETURNED`) | critical | What `getSecret` gave is part of the answer | | | Tokens returned (`CODE_TOKEN_ECHOED`) | low | `g.req.auth` or the headers are part of the answer | | | Sensitive values in the logs (`CODE_SENSITIVE_LOGGED`) | medium | A token, a password or a secret is printed to the logs | | | The users table read with its passwords (`CODE_USERS_TABLE_UNSELECTED`) | high in a custom API, medium elsewhere | A call reads the table of an auth provider without leaving its password column out | adds a `select` which leaves the column out | | The caller writes the filter (`CODE_CALLER_CONTROLS_FILTER`) | high | The filter (or the whole query) of a call of `g.sys.db` comes from the request as it is | | | Rows of persons read unscoped (`CODE_OWNED_ROWS_UNSCOPED`) | high | A table whose rows belong to persons is read with no filter on its owner column | | | A row by an id the caller sends (`CODE_ID_FROM_REQUEST`) | high for writes, medium for reads | The id comes from the request and the code never reads who the caller is | | | The body written as it is (`CODE_MASS_ASSIGNMENT`) | medium | Every column the caller sends is written : the owner, a role, a price | | | Error stacks returned (`CODE_ERROR_DETAILS_RETURNED`) | low | A stack trace is part of the answer | | | SQL put together from the request (`CODE_SQL_INJECTION`) | critical | A value of the request is pasted into the text of a statement | | | Commands run (`CODE_COMMAND_EXECUTION`) | critical with a value of the request, medium otherwise | `exec`, `system`, `subprocess`, `Runtime.exec` ... | | | Code made at run time (`CODE_DYNAMIC_EVAL`) | critical with a value of the request, medium otherwise | `eval`, `new Function` ... | | | An address the caller chooses (`CODE_SSRF`) | high | An outgoing call goes where the request says : the metadata of the cloud, the databases, the other services | | | A file the caller names (`CODE_PATH_TRAVERSAL`) | high | A path of the request reaches a file of the machine | | | TLS checks off (`CODE_TLS_VERIFY_DISABLED`) | high | `rejectUnauthorized: false`, `verify=False`, `InsecureSkipVerify: true` ... | turns the checks back on | | Weak password hashes (`CODE_WEAK_PASSWORD_HASH`) | medium | A password is hashed with MD5 or SHA-1 | | | Guessable random tokens (`CODE_INSECURE_RANDOM`) | medium | A token or a code is made with `Math.random`, `random.random`, `rand` ... | | | A hook which changes who the caller is (`CODE_HOOK_FORGES_IDENTITY`) | critical | `g.req.auth` or `g.req.isApiRequestFromUser` is written by a hook | | | A caller who turns the scoping off (`CODE_HOOK_CALLER_BYPASS`) | high | A hook returns before it scopes the rows when the request holds a header, a parameter or a field | | The key of a finding names the rule, the piece of code and what was found, not the line : a finding stays the same finding (and a skip stays a skip) when lines are added above it. ## Related - [APIs Security Report & Actions](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) · [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) · [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html) --- # Handle Role Based Permissions > Model every checkbox of your role screen as one small API Maker group, store group names in your own users table, and scope rows with a database level pre-hook. Source: https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html Sooner or later every business application grows this screen. Modules down the left, operations across the top, a checkbox wherever the two meet. ![Role permission matrix mapped to an API Maker group](https://docs.apimaker.dev/assets/images/role-based-permissions/rbac-checkbox-is-a-group.svg) --- Drawing the grid is a morning's work. Deciding what the backend should do when somebody ticks a box is what eats the rest of the sprint. --- ## Two gates, and you need both Every authenticated request in API Maker gets checked twice, and the two checks answer different questions. ![The API gate and the row gate](https://docs.apimaker.dev/assets/images/role-based-permissions/rbac-two-gates.svg) | | Question | Answered by | Enforced by | |---|---|---|---| | **Gate 1 — API gate** | May this caller call this endpoint at all? | The caller's **groups** | API Maker, before your code runs | | **Gate 2 — Row gate** | Of the rows this endpoint can reach, which ones are *this person's*? | A **pre-hook** that injects the user's id into the filter | Your hook, before the query runs | Drop either one and you have left a hole: - With only gate 1, `PURCHASE_INVOICE_VIEW` is allowed to call `get-by-id`, so anyone in that group can walk ids and read the whole company's invoices. - With only gate 2, someone who can see just their own rows can still call `remove-by-id` and delete them. The rest of this page sets both up for a Purchase Invoice module. --- ## How authorization works in API Maker Two identities ride along on every request, in two separate headers. People mix them up constantly, so it is worth being precise about which is which. | Header | Identity | Where it comes from | Read it with | |---|---|---|---| | `x-am-authorization` | The **API user** — *which application* | An API user created in API Maker | `g.req.auth.authAMUser` | | `x-am-user-authorization` | The **database user** — *which person* | A row in **your own** users table | `g.req.auth.authAMDB` | Groups hang off the API user and grants hang off the groups. When a request arrives, API Maker walks down this chain and stops at the first level that allows it: ```text instance → database → collection → supported API → field ``` Custom APIs, System APIs and WebSocket events do not cascade. They are flat lists: either the group names them or it does not. > Worth memorising, because it saves a lot of wasted debugging: `401` means the token is missing, malformed or expired. `403` means the token is fine and **no group grants that API**. If you are seeing 403s, go and fix the group. Do not log the user out, and do not send them back to the login screen. --- ## One group per checkbox This is the part people push back on, so let me make the case for it. Do not create a group called `Accountant`. Job titles are combinations, and combinations breed. The week HR invents "Junior Accountant, South Region" you are back in the console building another group, working out which APIs it needs, and hoping you remembered all of them. Make each group the size of one checkbox instead. A job title then becomes nothing more than a list. | Screen | API Maker group | |---|---| | Purchase › Purchase Invoices › View | `PURCHASE_INVOICE_VIEW` | | Purchase › Purchase Invoices › Create | `PURCHASE_INVOICE_CREATE` | | Purchase › Purchase Invoices › Edit | `PURCHASE_INVOICE_EDIT` | | Purchase › Purchase Invoices › Delete | `PURCHASE_INVOICE_DELETE` | | Purchase › Purchase Invoices › Export | `PURCHASE_INVOICE_EXPORT` | | Purchase › Purchase Invoices › Confirm | `PURCHASE_INVOICE_CONFIRM` | | Purchase › Debit Notes › View | `DEBIT_NOTE_VIEW` | | Purchase › Parcel Entry › View | `PARCEL_ENTRY_VIEW` | Small groups combine. `Accountant` is just `PURCHASE_INVOICE_VIEW,PURCHASE_INVOICE_CREATE,DEBIT_NOTE_VIEW`. That new job title is another list of the same groups, and nobody has to build anything. You will want one deliberately coarser group per module, for supervisors: | Group | Meaning | |---|---| | `PURCHASE_INVOICE_VIEW_ALL` | Same APIs as `PURCHASE_INVOICE_VIEW`, but exempt from row scoping. A manager or auditor sees the whole company's invoices | That exemption is one line of the row-scoping hook, which reads the groups of the person: see [the database level pre-hook](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html#the-database-level-pre-hook). --- ## Step 1 — Grant the group exactly what it needs Open **APIs Security › API group permission**, add `PURCHASE_INVOICE_VIEW`, and then tick almost nothing. ![Granting a single API to one group](https://docs.apimaker.dev/assets/images/role-based-permissions/rbac-group-grants.svg) Going back to the screenshot at the top of the page, the **View** checkbox on **Purchase Invoices** needs exactly two grants: | Category | Granted | |---|---| | Instance API | `erp_mongo › erp_db › purchase_invoices › SCHEMA_GET_BY_ID` | | Custom API | `purchase-invoice-print` | Everything else stays off, `SCHEMA_GET_ALL` and `SCHEMA_POST_QUERY` included. A detail screen opens one invoice at a time, so it has no business calling an API that hands back all of them. The list screen gets its own group for that. The full set for the Purchase Invoice module: | Group | Instance APIs granted | Custom APIs granted | |---|---|---| | `PURCHASE_INVOICE_VIEW` | `SCHEMA_GET_BY_ID` | `purchase-invoice-print` | | `PURCHASE_INVOICE_LIST` | `SCHEMA_GET_ALL` | — | | `PURCHASE_INVOICE_CREATE` | `SCHEMA_POST_BULK_INSERT` | — | | `PURCHASE_INVOICE_EDIT` | `SCHEMA_PUT_UPDATE_BY_ID` | — | | `PURCHASE_INVOICE_DELETE` | `SCHEMA_DEL_DELETE_BY_ID` | — | | `PURCHASE_INVOICE_EXPORT` | `SCHEMA_GET_ALL_STREAM` | `purchase-invoice-export-xlsx` | | `PURCHASE_INVOICE_CONFIRM` | — | `purchase-invoice-confirm` | `PURCHASE_INVOICE_CONFIRM` is worth a second look. Confirming an invoice is not CRUD: it checks stock, posts ledger entries and locks the document. So it is a Custom API, and a group can grant one Custom API and nothing besides. The checkbox on screen still lines up with exactly one group. Never switch the broad flags on for an application group. `allCollections` does not only grant what exists today, it grants whatever you add next month and next year too. That is how a `salaries` table ends up readable by a group nobody has looked at in two years. ### Field level: hiding columns inside a row Groups control individual fields as well. Anything a group cannot read gets stripped out of the response, `deep`-populated documents included. That last part is the one people forget. ```json { "fields": { "erp_mongo": { "erp_db": { "purchase_invoices": { "landed_cost": { "allowRead": false, "allowWrite": false }, "supplier_margin": { "allowRead": false, "allowWrite": false }, "invoice_total": { "allowRead": true, "allowWrite": false } } } } } } ``` `PURCHASE_INVOICE_VIEW` can now open an invoice without ever learning what the company paid for the goods. --- ## Step 2 — You do **not** create groups or users in API Maker at runtime This one trips up almost everyone arriving from another stack. ![The groups column drives everything](https://docs.apimaker.dev/assets/images/role-based-permissions/rbac-groups-column.svg) There is no "create group" API to call when you onboard a customer, no API user per employee, and no nightly job pushing your permission tables into API Maker's. You write the groups once, in the console, and they go into Git along with everything else. Handing someone a role at runtime is an `UPDATE` on a column in your own table, which the admin screen you were already building can do. ### Add a `groups` column to your users table **erp_users schema** ```ts import { ISchemaType, EType, ISchemaProperty, IPropertyValidation } from 'types'; let schema: ISchemaType = { _id: EType.objectId, user_name: { __type: EType.string, validations: { required: true } }, password: { __type: EType.string, validations: { required: true }, conversions: { hashing: true } }, passwordChangedAt: { __type: EType.number }, department: { __type: EType.string }, is_active: { __type: EType.boolean }, // 👇 the whole role model, in one column groups: { __type: EType.string } }; module.exports = { schema }; ``` Rows look like this: | `_id` | `user_name` | `groups` | |---|---|---| | `101` | nikita | `PURCHASE_INVOICE_VIEW` | | `102` | rakesh | `PURCHASE_INVOICE_VIEW,PURCHASE_INVOICE_CREATE` | | `103` | meera | `PURCHASE_INVOICE_VIEW_ALL,DEBIT_NOTE_VIEW` | | `104` | root | `*` | `*` is shorthand for *every group the API user has*. Keep it for your own internal service account. ### Point API Maker at that column Declare a DB **auth provider** on that table in **Security › Auth Providers**, and name the column in `groupsColumn`. The whole page: [Auth of a database user](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html). **Basic Info of the provider erp_users_tg** ```typescript import * as T from 'types'; let dbTokenGenerator: T.IAuthTokenAMDB = { name: 'erp_users_tg', instance: 'erp_mongo', database: 'erp_db', collection: 'erp_users', usernameColumn: 'user_name', passwordColumn: 'password', // A password change ends every token made before it. passwordChangedAtColumn: 'passwordChangedAt', // 👇 API Maker reads this column and resolves the grants itself groupsColumn: 'groups', // The columns of the person in g.req.auth.authAMDB select: { _id: 1, user_name: 1, department: 1, groups: 1 }, }; module.exports = dbTokenGenerator; ``` Then make the APIs of `erp_db` ask for that provider, in the settings of the database. A [table](https://docs.apimaker.dev/v4/docs/settings/collectionSettings.html) or an [API](https://docs.apimaker.dev/v4/docs/settings/apiSettings.html) can name its own, and `common.authProviders` of the [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.html#the-keys-of-common) is the default of every API. **Database settings of erp_db** ```typescript import * as T from 'types'; let settings: T.IInstanceApiSettingsTypes = { apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, authProviders: ['erp_users_tg'], }; module.exports = settings; ``` That is the whole wiring. From here on: - The user logs in, and API Maker reads `groups` off their row. - Access becomes the **union** of those groups' grants. - Change the column, and the user's next calls follow it. The token carries no groups: API Maker reads them off the row. > Keep `_id` in `select`. The hook in the next step needs it to work out who owns a row, and anything you leave out of `select` is not in `g.req.auth.authAMDB`. Keep `groups` there too, for the supervisor rule of that hook. You do not need to check which APIs the groups grant: API Maker reads `groupsColumn` and enforces it for you. The same `groupsColumn` field turns up on the [Google](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html#google), [AWS Cognito](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html#aws) and [Azure AD](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html#azure) configurations, under `groupsDataSource`. Swap your own login screen for single sign-on later and the groups half of this does not change. --- ## Step 3 — Log in and mint both tokens One public Custom API, and it is the only place an API user password should ever show up. It hands the client both headers in a single round trip. **TypeScript** **Custom API: login (apiAccessType: IS_PUBLIC)** ```typescript import * as T from 'types'; import * as db from 'db-interfaces'; const INSTANCE = 'erp_mongo'; const DATABASE = 'erp_db'; const USERS = 'erp_users'; async function main(g: T.IAMGlobal) { const userName = g.req.body.user_name; const password = g.req.body.password; if (!userName || !password) { throw [{ code: T.EStatusCode.BAD_REQUEST, message: 'user_name and password are required.' }]; } const user: db.erp_mongo.erp_db.IErpUsers = await g.sys.db.getById({ instance: INSTANCE, database: DATABASE, collection: USERS, primaryKey: 'user_name', id: userName, // 👇 Ask for the columns this API uses, and nothing else. // The password column is never one of them. select: '_id,user_name,department,groups,is_active' }); if (!user || user.is_active !== true) { throw [{ code: T.EStatusCode.UNAUTHORIZED, message: 'Invalid credentials.' }]; } // The API user password lives in the secret, never in code and never in the frontend. const apiUserPassword = await g.sys.system.getSecret('common.apiUserPasswords.erp_app'); const [app, person] = await g.sys.system.getToken([ // No name : the token of the API user { u: 'erp_app', p: apiUserPassword }, // The DB auth provider 'erp_users_tg' checks the password against the erp_users table { name: 'erp_users_tg', u: userName, p: password } ]); return { amToken: app.token, // → send as x-am-authorization amDbToken: person.token, // → send as x-am-user-authorization user: { _id: user._id, user_name: user.user_name, groups: user.groups } }; } module.exports = main; ``` `select` earns its keep there. Leave it out and the lookup drags the whole row into your sandbox, password hash and all, where it can end up in a log line or an error payload without anyone meaning it to. `getToken` checks the credentials against that same table on its own, so this code never has a reason to hold the hash. Pass a comma-separated list of the columns you want, or `-password` to drop just that one. Sending `groups` back lets the frontend grey out buttons the user cannot use. That is a usability thing, nothing more. The server has already made the real decision, and it makes it again on every call. --- ## Step 4 — Row level security with a database level pre-hook Groups answered "can rakesh call `get-by-id` on `purchase_invoices`?". They cannot answer "is `PI-1042` rakesh's invoice?", because that is a fact about the data rather than about configuration. Pre-hooks handle the second question. ### Pick the scope Pre-hooks attach at four scopes and run outermost first. Post-hooks run the same chain in reverse. ```text pre : instance → database → collection → api post : api → collection → database → instance ``` | Scope | Applies to | |---|---| | **Instance** | Every API on every collection in every database of that instance | | **Database** | Every API on every collection in that database | | **Collection** | Every API on that collection | | **API** | Just that one endpoint | Put row scoping at the **database** scope. One hook then covers `purchase_invoices`, `debit_notes`, `parcel_entries`, and whatever collection somebody adds next quarter without mentioning it. That last case is the one that kills the per-endpoint `if`. Attach it under **Instances → `erp_mongo` → `erp_db` → Pre-hooks**. ### What a pre-hook can change, per API Worth knowing, because the generated APIs do not all keep their filter in the same place. By the time your hook runs, `find` has already been parsed into a plain object, so mutating it changes the query. | API | Where the filter lives | How to scope it | |---|---|---| | `SCHEMA_GET_ALL`, `SCHEMA_GET_ALL_STREAM` | `g.req.query.find` | merge the owner into `find` | | `SCHEMA_POST_QUERY`, `SCHEMA_POST_QUERY_STREAM`, `SCHEMA_POST_QUERY_DELETE`, `SCHEMA_UPDATE_MANY`, `SCHEMA_POST_DISTINCT_QUERY`, `SCHEMA_ARRAY_OPERATIONS`, `SCHEMA_POST_COUNT` | `g.req.body.find` | merge the owner into `find` ; for update many, also stamp the owner into `updateData` | | `SCHEMA_GET_BY_ID`, `SCHEMA_PUT_UPDATE_BY_ID`, `SCHEMA_PUT_REPLACE_BY_ID`, `SCHEMA_DEL_DELETE_BY_ID` | `g.req.query.find`, **ANDed onto the primary key** | merge the owner into `find` ; for update and replace, also stamp the owner into the body | | `SCHEMA_POST_BULK_INSERT`, `SCHEMA_MASTER_SAVE` | `g.req.body` | stamp the owner server-side | | `SCHEMA_POST_AGGREGATE`, `SCHEMA_GET_DISTINCT` | a pipeline (aggregate), the column in the URL (distinct) | cannot be scoped generically, so do not grant them | The third row catches people out. `get-by-id` and its write siblings take an id in the URL and the caller has no way to send a `find` of their own, but the engine still folds whatever your hook leaves on `g.req.query.find` into the filter it builds around the primary key, for MongoDB and the SQL databases alike. With the hook below, `get-by-id/PI-1042` called by rakesh reads: ```json { "_id": "PI-1042", "created_by": 102 } ``` One filter therefore scopes every row-driven API, so you do not have to load the row and compare owners yourself. You should not, either, since it costs a database round trip on every call. You also get the better failure mode without asking for it. A row that is not yours never matches, so the response comes back empty rather than 403, and there is nothing for an attacker to distinguish between "no such id" and "not your id". ### The database level pre-hook **TypeScript** **Database pre-hook on erp_mongo › erp_db** ```typescript import * as T from 'types'; // ── Which collections are owned by a user, by which column, and which group reads every row ────────────── // A collection that is not in this map is not row-scoped at all. const OWNED = new Map([ ['purchase_invoices', { column: 'created_by', seesAll: 'PURCHASE_INVOICE_VIEW_ALL' }], ['debit_notes', { column: 'created_by', seesAll: 'DEBIT_NOTE_VIEW_ALL' }], ['parcel_entries', { column: 'entered_by', seesAll: 'PARCEL_ENTRY_VIEW_ALL' }] ]); // ── apiId → where that API carries its filter ────────────── // One O(1) lookup per request instead of scanning arrays. const SCOPE_BY_API = new Map([ // API Maker ANDs g.req.query.find onto the primary key for the *-by-id APIs, // so one filter covers the list, the single read, and the writes alike. ['SCHEMA_GET_ALL', 'QUERY_FIND'], ['SCHEMA_GET_ALL_STREAM', 'QUERY_FIND'], ['SCHEMA_GET_BY_ID', 'QUERY_FIND'], ['SCHEMA_PUT_UPDATE_BY_ID', 'QUERY_FIND'], ['SCHEMA_PUT_REPLACE_BY_ID', 'QUERY_FIND'], ['SCHEMA_DEL_DELETE_BY_ID', 'QUERY_FIND'], ['SCHEMA_POST_QUERY', 'BODY_FIND'], ['SCHEMA_POST_QUERY_STREAM', 'BODY_FIND'], ['SCHEMA_POST_QUERY_DELETE', 'BODY_FIND'], ['SCHEMA_UPDATE_MANY', 'BODY_FIND'], ['SCHEMA_POST_DISTINCT_QUERY', 'BODY_FIND'], ['SCHEMA_ARRAY_OPERATIONS', 'BODY_FIND'], ['SCHEMA_POST_COUNT', 'BODY_FIND'], ['SCHEMA_POST_BULK_INSERT', 'STAMP_OWNER'], ['SCHEMA_MASTER_SAVE', 'STAMP_OWNER'] ]); // ── The APIs which only read : a supervisor sees every row with these ────────────── const READS = new Set([ 'SCHEMA_GET_ALL', 'SCHEMA_GET_ALL_STREAM', 'SCHEMA_GET_BY_ID', 'SCHEMA_POST_QUERY', 'SCHEMA_POST_QUERY_STREAM', 'SCHEMA_POST_DISTINCT_QUERY', 'SCHEMA_POST_COUNT' ]); async function main(g: T.IAMGlobal) { // 1. Only scope real HTTP traffic. Custom APIs, events and schedulers calling // g.sys.db run under their own rules. if (!g.req.isApiRequestFromUser) return; const owned = OWNED.get(g.req.params.collection); if (!owned) return; // not a user-owned collection // 2. Which person is acting? Not the application — the person. // API Maker has already rejected the request if the user token is missing. const person = g.req.auth.authAMDB; const apiId = g.req.reqInfo.apiInfo.id; // A supervisor reads every row. The groups come from the groups column of the person ; '*' is every group. const groups = String(person.groups || '').split(','); if (READS.has(apiId) && (groups.includes(owned.seesAll) || groups.includes('*'))) return; const scope = { [owned.column]: person._id }; // MERGE, never replace. $and keeps whatever the caller asked for and adds ours // on top, so a caller can never widen the scope by sending its own created_by. const merge = (existing) => (existing && Object.keys(existing).length ? { $and: [existing, scope] } : scope); // The owner of a row is written by the server, never by the caller. const stamp = (row) => { if (row && typeof row === 'object') row[owned.column] = person._id; }; // 3. One lookup decides where the filter goes. switch (SCOPE_BY_API.get(apiId)) { // get-all, get-by-id, update-by-id, replace-by-id, remove-by-id. // A row that is not yours is simply never matched — no extra round-trip, // and no 404-vs-403 difference for an attacker to probe. case 'QUERY_FIND': g.req.query.find = merge(g.req.query.find); stamp(g.req.body); // update-by-id, replace-by-id : the owner can not be handed over return; case 'BODY_FIND': g.req.body.find = merge(g.req.body.find); if (g.req.body.updateData) stamp(g.req.body.updateData); // update-many return; // Inserts and master save: stamp the owner on the server. Never trust the body. case 'STAMP_OWNER': { const rows = Array.isArray(g.req.body) ? g.req.body : [g.req.body]; for (const row of rows) stamp(row); return; } // Fail closed. Aggregate and distinct cannot be scoped generically, // so if one of them is ever granted by mistake, it stops here. default: throw [{ code: T.EStatusCode.FORBIDDEN, message: 'This API is not available for row-scoped users.' }]; } } module.exports = main; ``` A few details in there are easy to skim past. The `isApiRequestFromUser` guard lets internal calls through untouched. Custom APIs, events and schedulers reaching the collection via `g.sys.db` run under their own rules, and a scheduler rebuilding a report is not a user at all, so there is no `authAMDB` to scope by. The merge uses `$and` rather than a spread, and that is deliberate. `{ ...find, created_by: userId }` looks like the same thing and is not: if the caller sends `find={created_by:{$ne:null}}` their key survives next to yours, and which one wins comes down to operator precedence. `$and` applies both conditions no matter what, so the caller cannot widen the result set. The lookup tables are a `Map` and a `Set`. The hook runs on every single request against the database, so scanning arrays with `indexOf` is work you would be paying for constantly and getting nothing back for. A `Map` is also the safer shape here: the collection is keyed off the URL, and a plain object would happily hand back `constructor` or `toString` for a collection with one of those names, sailing straight past the `if (!owned)` guard. Supervisors are one line. The person's groups come from the `groups` column, through `g.req.auth.authAMDB`, and `PURCHASE_INVOICE_VIEW_ALL` lifts the scope on the reads only. A supervisor who also holds `PURCHASE_INVOICE_EDIT` still edits their own invoices alone. `*`, every group, counts as holding it. The `default` branch throws rather than falling through. A hook that ends in an implicit `return` quietly allows every API added after it was written. This way a mis-grant turns into a support ticket instead of a breach. The owner is written by the server on every write : the rows of an insert or a master save, the body of an update or a replace by id, and the `updateData` of an update many. A caller who sends `created_by: 7` cannot hand an invoice to someone else. The map lists the APIs of tables with a schema (`SCHEMA_…`). The generated APIs without a schema (`GEN_…`, under `/api/gen`) reach the `default` branch, so a row-scoped person gets `403` from them. Add their ids next to the others if those people need them : the hook of the [APIs Security Report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) lists both families. And because the scope goes into the filter rather than into a check afterwards, someone else's invoice just does not match. The caller cannot tell "no such id" from "not your id", which is what you want and costs nothing to get. ### Gating a hook to specific groups Hooks can carry a `groupNames` list. Give it one and the hook only runs when the **API user** of the call, the one of `x-am-authorization`, is in one of those groups. | Hook has `groupNames`? | API user's groups intersect? | Runs? | |---|---|---| | No | — | always | | Yes | Yes | yes | | Yes | No | skipped | | Yes | API user has no groups | skipped | That is how you row-scope customer-facing API users while leaving an internal integration account alone, with no `if` inside the hook. `groupNames` never looks at the person. The `groups` column decides which APIs the person may call, not which hooks run, and every person of the application comes through the same API user. So the supervisor exemption stays the line in the hook that reads `g.req.auth.authAMDB`. Put `PURCHASE_INVOICE_VIEW_ALL` in `groupNames` instead and the hook skips for every person whose API user lacks that group, and a row-scoping hook that does not run scopes nothing. --- ## Step 5 — Try to break it Nobody should believe a permission model until they have tried to get round it. Run these. ```bash # 1. log in — one call, both tokens TOKENS=$(curl -s -X POST "$BASE/api/custom-api/erp/login" \ -H 'content-type: application/json' \ -d '{"user_name":"rakesh","password":""}') AM=$(echo "$TOKENS" | jq -r '.data.amToken') USR=$(echo "$TOKENS" | jq -r '.data.amDbToken') # 2. his own invoice → 200 curl -s -o /dev/null -w '%{http_code}\n' \ "$BASE/api/schema/erp/erp_mongo/erp_db/purchase_invoices/get-by-id/PI-1042" \ -H "x-am-authorization: $AM" -H "x-am-user-authorization: $USR" # 3. somebody else's invoice → no data (the row gate: it never matches) curl -s "$BASE/api/schema/erp/erp_mongo/erp_db/purchase_invoices/get-by-id/PI-2001" \ -H "x-am-authorization: $AM" -H "x-am-user-authorization: $USR" \ | jq '.data' # → null, same as an id that does not exist # 4. an API his groups never granted → 403 (the API gate) curl -s -o /dev/null -w '%{http_code}\n' -X DELETE \ "$BASE/api/schema/erp/erp_mongo/erp_db/purchase_invoices/PI-1042" \ -H "x-am-authorization: $AM" -H "x-am-user-authorization: $USR" # 5. a collection nobody granted → 403 curl -s -o /dev/null -w '%{http_code}\n' \ "$BASE/api/schema/erp/erp_mongo/erp_db/salaries" \ -H "x-am-authorization: $AM" -H "x-am-user-authorization: $USR" # 6. a field the group cannot read is absent, not null curl -s "$BASE/api/schema/erp/erp_mongo/erp_db/purchase_invoices/get-by-id/PI-1042" \ -H "x-am-authorization: $AM" -H "x-am-user-authorization: $USR" \ | jq '.data | has("landed_cost")' # → false # 7. a list call cannot be widened from the client curl -s "$BASE/api/schema/erp/erp_mongo/erp_db/purchase_invoices?find={created_by:{\$ne:null}}" \ -H "x-am-authorization: $AM" -H "x-am-user-authorization: $USR" \ | jq '[.data[].created_by] | unique' # → ["102"] ``` If any of them comes back wider than you expected, your grants are wider than you think they are. Three more things that help here: - Send `x-am-meta: true` and the response tells you which groups were consulted and whether each one granted access. - Every API user gets a Swagger document covering exactly the APIs that user can reach. Diff it in CI, and a group that quietly got wider shows up as a comment on a pull request rather than as an incident six months later. - **Security › APIs Security Report & Actions** reads every group, auth provider and pre-hook of the account and flags most of the mistakes below: a collection no hook scopes, a spread merge, an owner taken from the body, `allCollections`, `allSystemApis`, a password in the token. The missing hook and the broad grants are fixed in one click, the others open the place to change. See [APIs Security Report & Actions](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html). --- ## Putting it together | Question | Where it is answered | |---|---| | Which application is calling? | `x-am-authorization` — the API user | | Which person is acting? | `x-am-user-authorization` — a row in **your** table | | Which roles does that person hold? | The `groups` column on that row | | What may those roles call? | API group permission, in API Maker, authored once | | Which **rows** may they touch? | A database level pre-hook | | Which **fields** may they see? | Field level access on the group | What makes this hold up over time is that none of the enforcement lives in your endpoint code. A new collection is denied until some group grants it, and a new endpoint on an existing collection picks up the database hook the moment it exists. There is nothing you have to remember to write. --- ## Common mistakes | Mistake | What happens | Fix | |---|---|---| | One group per job title | Group count grows with the org chart; every new title is a fresh audit | One group per checkbox; job titles are comma-separated lists | | `allCollections: true` on an application group | Every table you add later is instantly reachable | Allow-list collections explicitly | | `allSystemApis: true` | Grants `EXECUTE_PLAIN_QUERY` and `GET_SECRET` | Never set it; allow-list individual system APIs if you truly need them | | Merging the owner with a spread instead of `$and` | A crafted `find` can widen the result set | `find = { $and: [caller, ours] }` | | Loading the row to compare owners on `get-by-id` | An extra database round-trip on every call, for a check the engine already does | Set `g.req.query.find`; API Maker ANDs it onto the primary key | | Omitting `_id` from `select` | The hook cannot see who is calling | Keep `_id` in `select` | | Putting a person's group in a hook's `groupNames` | `groupNames` is compared with the groups of the API user, so the hook skips for every person of the application or for none | Read the person's groups from `g.req.auth.authAMDB` inside the hook | | Trusting `created_by` from the request body | A caller can create rows owned by someone else | Stamp the owner in the pre-hook | | Row scoping enforced in a Custom API only | Schedulers and test cases bypass authorization entirely | Enforce at the hook, and treat a scheduler as being as privileged as your database credentials | --- ## Related reading - [APIs Security Report & Actions](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) - [API group permission](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) - [API user permission](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) - [Authorization of AM database user](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) - [Pre-hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html) · [Post-hook](https://docs.apimaker.dev/v4/docs/apis-all/hooks/postHook-api.html) - [Global object `g`](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html) - [Secrets](https://docs.apimaker.dev/v4/docs/secrets/secrets.html#the-keys-of-common) - [Single sign on authentication](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.md) --- # API Group Permissions > Groups are the permission unit of API Maker - which tables, APIs and fields, which custom and system APIs, which WebSocket events - given to API users and named in the groups column of your own users. Source: https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html A group is a list of what may be called : tables and their APIs, fields readable and writable, custom APIs, system APIs, WebSocket events. An [API user](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) gets groups ; a person of your users table gets groups through the `groupsColumn` of its [auth provider](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html). A call passes when the API user grants it and, when a person is involved, the person too. > Diagram : Two tokens, two gates : the API user (which application) and the person (which rows) > > Two identities on every request : the API user token from the token API in x-am-authorization tells which application calls, and the token of a person from a token generator in x-am-user-authorization tells who the person is. Groups of the API user decide which APIs, tables and fields may be called ; pre hooks decide which rows belong to the person. | | | |---|---| | Page | `Security → API Group Permissions` : the groups on the left, the tree of the account on the right. | | Grants | Instances → databases → tables → APIs, and the fields of a table with **R** and **W** ; custom APIs ; system APIs ; WebSocket events. | | Shortcuts | **All APIs**, **All Fields** and the checkbox of a column check everything below it. New tables and APIs are covered by an "all" grant. | | Refused with | `403` when no group of the caller grants the API ; a field not readable is absent from the answer, a field not writable is refused on save. | ## Make a group 1. **Add New**, give a unique name, **Save Group**. **Clone** copies an existing group. 2. Open the tree. Each API shows how it is reachable, from its settings : **Public**, **AM-User** (API user token), **DB-User** (person token), **Google**, **Azure**, or **No-Access**. 3. Check the APIs and, under a table, the fields : **R** to read them, **W** to write them. 4. **Save group settings**. **Active API users in this group** shows who has it. ## Give it - To an API user : the groups list on the [API User Permissions](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) page. - To a person : write the group names, comma separated, in the column named by `groupsColumn` of the auth provider. `*` gives the person every group of the API user. ## Good to know - The pre and post hooks of an API can be limited to some groups : they run only for callers of those groups. - The [APIs Security Report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) lists the groups which reach too far. - Groups go to Git : the same permissions on every environment. ## Related - [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html) · [API user permissions](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) · [Auth of database user](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) --- # API User Permissions > An API user is the identity of an application which calls API Maker - its token goes in x-am-authorization, its groups decide the APIs reachable, its password can live in the secret, and it can publish Swagger docs of its APIs. Source: https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html An API user is an application : the web app, the mobile app, a partner. It signs in with a username and a password, gets a token, and every call carries that token in `x-am-authorization`. Its groups decide which APIs the application can call at all. | | | |---|---| | Page | `Security → API User Permissions` : the API users on the left, the groups of the selected one on the right. | | Fields | Name, username, password, or **Enable Password From Secret Path** with the path of the password in the [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.html) (`common.apiUserPasswords.default`). | | Token | [`POST /api/system-api//token`](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html) with `{ "u": "username", "p": "password" }`. | | Swagger | **Enable Swagger Docs** and a **Swagger Token** publish the documentation of the APIs this API user can call : **View Swagger JSON** opens it. | | New account | An API user `default`, password `12345` from the secret, with the group `Default`. | ## Set one up 1. **Add New**, give the name and the username. 2. Type a password, or enable the password from a secret path : the password then lives in the secret of each environment, not in Git. 3. Check the [groups](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) it gets. An API user with several groups can call what any of them grants. 4. **Save User**. The application gets its token with the username and the password. ## Swagger docs per API user - Generate a random Swagger token and enable the docs : the URL shown answers with the OpenAPI document of exactly the APIs this API user can call, database, custom and system APIs alike. - Share it with the team which builds on that API user. Disable the docs, or generate a new token, to stop an old URL. ## Good to know - Changing the password invalidates the tokens made before. - API users go to Git ; with a password from a secret path, the password does not. ## Related - [Get token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html) · [API group permissions](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) · [Auth of API user](https://docs.apimaker.dev/v4/docs/authorization/AMApiUser.html) --- # Sign-in Providers > Let people sign in to your app with Google, Azure AD (Microsoft Entra ID), AWS Cognito, your own users table or a system of your own - API Maker checks their token, finds the person in a table of yours and runs the call with their groups. Source: https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html Your app signs people in, and sends the token it gets to API Maker with every call. An **auth provider** checks that token, finds the person in a table of yours, and runs the call with the [groups](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) of that person. Make one on `Security → Auth Providers`. | Provider | The app sends | Your code reads | |---|---|---| | [Your users table](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) | `x-am-user-authorization` : a token of API Maker, from a username and a password | `g.req.auth.authAMDB` | | [Google](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html#google) | `x-google-authorization` : the id token of Google | `g.req.auth.authGoogle` | | [Azure AD](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html#azure) (Microsoft Entra ID) | `x-azure-authorization` : the token of Microsoft | `g.req.auth.authAzure` | | [AWS Cognito](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html#aws) | `x-aws-authorization` : the token of the user pool | `g.req.auth.authAWS` | | [A provider of your own](https://docs.apimaker.dev/v4/docs/authorization/sign-in-providers.html#custom) | `x-custom-authorization` : whatever your system issues | `g.req.auth.authCustom` | The call carries the token of the [API user](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) in `x-am-authorization` as well : the application and the person both need a group which grants the API. ## Google **The provider** ```typescript import * as T from 'types'; let googleTokenGenerator: T.IAuthTokenGoogle & { name: string; testObj?: any; } = { name: 'google_token_generator', clientId: '.apps.googleusercontent.com', // the aud of your tokens : a list when several apps sign in sourceFieldOfUniqueId: 'sub', // or 'email' groupsDataSource: { instance: 'mongodb', database: 'shop', collection: 'users', targetFieldForUniqueId: 'google_sub', groupsColumn: 'groups', select: { name: 1, email: 1 } }, }; module.exports = googleTokenGenerator; ``` Google signs the id tokens of every application with the same keys. `clientId` is the application a token has to be made for (its `aud` claim) : the OAuth client id of your app, or a list of them. Without `clientId`, a token Google made for any other site is taken too : that site could call your APIs as each person who signed in there. The [API Security Report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) lists such a provider as **takes the Google tokens of any application** (high). ## Azure AD **The provider** ```typescript import * as T from 'types'; let azureTokenGenerator: T.IAuthTokenAzureAD & { name: string; testObj?: any; } = { name: 'azure_token_generator', appId: '', tenant: '', audience: '', // the aud of your tokens issuer: 'https://login.microsoftonline.com//v2.0', // the iss of your tokens maxRetries: 3, // fetching the signing keys sourceFieldOfUniqueId: 'oid', // or 'preferred_username' for the email groupsDataSource: { instance: 'mongodb', database: 'shop', collection: 'users', targetFieldForUniqueId: 'azure_oid', groupsColumn: 'groups', select: { name: 1, email: 1 } }, }; module.exports = azureTokenGenerator; ``` Microsoft signs the tokens of every application with the same keys. The keys prove that Microsoft made a token, not that it was made for your application : - `audience` is the application a token has to be made for (its `aud` claim) : the application (client) id, or the Application ID URI (`api://...`) when your tokens carry that. - `issuer` is the directory a token has to come from (its `iss` claim) : `https://login.microsoftonline.com//v2.0` for tokens of version 2, `https://sts.windows.net//` for tokens of version 1. - Either can be a list : a token which carries one of its entries is taken. - Left empty (or an empty list), the claim is not checked. A token Microsoft made for another application is then taken too, and the [API Security Report](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html) lists the provider as **takes the Azure tokens of any application** (high). Paste one of your tokens in [jwt.ms](https://jwt.ms) to read its `aud` and `iss`. ## AWS Cognito **The provider** ```typescript import * as T from 'types'; let awsTokenGenerator: T.IAuthTokenAWS & { name: string; testObj?: any; } = { name: 'aws_token_generator', cognitoUserPoolId: 'us-east-1_xxxxxx', region: 'us-east-1', tokenUse: 'access', // 'access' | 'id' : which Cognito token the app sends tokenExpiration: 3600000, // milliseconds, at most 3600000 sourceFieldOfUniqueId: 'sub', // the claim which identifies the person groupsDataSource: { instance: 'mongodb', database: 'shop', collection: 'users', targetFieldForUniqueId: 'cognito_sub', groupsColumn: 'groups', select: { name: 1, email: 1 } }, }; module.exports = awsTokenGenerator; ``` - `tokenUse` is the Cognito token the app sends, `access` or `id`. The user pool and the region come from AWS. ## The groups of the person - `sourceFieldOfUniqueId` is the claim of the token which identifies the person (an email, `sub`, `oid`). - `groupsDataSource` names the table of yours which holds that person : `targetFieldForUniqueId` is the column with the same value, `groupsColumn` the comma separated [groups](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) of the person, `select` the columns to put in `g.req.auth`. - Without `groupsDataSource`, the token is checked but the person gets no group of its own : only the API user decides. ## Require it - `authProviders: ['google_token_generator']` (the `name` of the provider) in the settings of the APIs, tables or databases, or in `common.authProviders` of the secret. - The call carries the API user token in `x-am-authorization`, and the token of the provider in its header. - `testObj` in the provider is the person the API testing page pretends to be. ## A provider of your own For any other system, write a **Custom** provider : a token generator, which answers the token API for it, and a token validator, which says whether a token is valid, in any of the [languages](https://docs.apimaker.dev/v4/docs/languages/index.html). The app sends its token in `x-custom-authorization`, and your code reads the result in `g.req.auth.authCustom`. See [the example](https://docs.apimaker.dev/v4/examples/req/auth/authCustom.html). ## Related - [Auth of a database user](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) · [API group permissions](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) · [Who calls : g.req.auth](https://docs.apimaker.dev/v4/examples/req/auth/auth.html) --- # Auth of a Database User > Sign the people of your own users table in with API Maker - a DB auth provider names the table and its columns, the token API gives the person a token, x-am-user-authorization carries it, and the groups column decides what the person may call. Source: https://docs.apimaker.dev/v4/docs/authorization/AMDB.html Your users are rows of a table of yours. An **auth provider** of type DB tells API Maker where : the table, the username column, the password column, the groups column. From then on the [token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html) signs those people in, and every call carries their token in `x-am-user-authorization`. > Diagram : Two tokens, two gates : the API user (which application) and the person (which rows) > > Two identities on every request : the API user token from the token API in x-am-authorization tells which application calls, and the token of a person from a token generator in x-am-user-authorization tells who the person is. Groups of the API user decide which APIs, tables and fields may be called ; pre hooks decide which rows belong to the person. | | | |---|---| | Page | `Security → Auth Providers` : one provider per users table. Types : DB, AWS Cognito, Azure AD, Google, Custom. | | Header | `x-am-user-authorization: ` | | Get it | `POST /api/system-api//token` with `{ "name": "", "u", "p" }` | | Require it | `authProviders: ['']` in the settings of an API, a table, a database, or in `common.authProviders` of the secret. | | In code | `g.req.auth.authAMDB` : the row of the person, without its password. | ## 1. Declare the provider **Basic Info of the provider** ```typescript import * as T from 'types'; let dbTokenGenerator: T.IAuthTokenAMDB = { name: 'users_tg', instance: 'mongodb', database: 'shop', collection: 'users', // table: '…' for SQL usernameColumn: 'email', passwordColumn: 'password', // a column with hashing works : the password sent is hashed the same way and compared passwordChangedAtColumn: 'passwordChangedAt', // optional : the token carries this value instead of the password fingerprint groupsColumn: 'groups', // comma separated group names ; '*' = every group of the API user expiresInSeconds: 259200, runOnNativeProcess: false, // where the Fields Generator runs }; module.exports = dbTokenGenerator; ``` - The Basic Info of a provider is always TypeScript, which API Maker [runs itself](https://docs.apimaker.dev/v4/docs/languages/index.html#settings-run) when you save : its tab shows TypeScript with a lock. The Fields Generator is written in any [language](https://docs.apimaker.dev/v4/docs/languages/index.html#basic-info). - The `hashing` conversion of a [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html#3-conversions-clean-the-value-first) is HMAC SHA-256 with one key of the server (the `nonce` of the secret). It keeps the passwords out of sight, but it is fast and has no salt per person : it is not a password hash like Argon2id or bcrypt. Whoever has the table and that key can guess weak passwords quickly, so keep the secret safe and ask for long passwords. - `select` limits the columns of the person put in `g.req.auth.authAMDB` ; `condition` adds a filter to the lookup (`{ active: true }`). - `passwordChangedAtColumn` : update it whenever a password changes, and every token made before stops working, refresh tokens included. A token made while the column of the person is still empty ends with its first value. - Without that column the token carries a fingerprint of the stored password (`amfp1:…`, a keyed hash made with the signing secret of the server), never the password or its hash : a changed password ends the tokens made before, and nothing about the password can be read out of a token. ## 2. Require it on the APIs **Settings of an API, a table or a database** ```typescript let settings: T.IInstanceApiSettingsTypes = { apiAccessType: T.EAPIAccessType.TOKEN_ACCESS, authProviders: ['users_tg'], }; module.exports = settings; ``` - Without `authProviders` in any settings, the APIs need what `common.authProviders` of the [secret](https://docs.apimaker.dev/v4/docs/secrets/secrets.html) names ; nothing there means the API user token alone. - Several DB providers in the list : a token of any of them works. A provider of another type in the list (Google…) adds its own header to what the call must carry. - A token works only for the provider which made it, in the account which made it. The token of another provider, of another account or of an API user answers `401` : `Invalid token provided in 'x-am-user-authorization' header.`, even when that table has a person with the same username and password. - Your other admin user accounts accept the token too when the root setting **Allow API Maker User's Token Across Admin Users** is on (`Root Settings → Deployment Settings → Security → Your APIs`) : the account serving the request needs a provider of the same name, the token must still be valid in the account which made it, and both users tables must keep the same password for that username. The person of the request is the row of the account serving it, with its own groups ; a refresh is asked of the account which made the token. - A call without the token answers `401` : `Please provide 'x-am-user-authorization' token in request headers.` A token whose person is not in the table any more : `401`, `Token user not found in 'mongodb' -> 'shop' -> 'users'.` ## 3. Sign in **POST /api/system-api/admin/token** ```json { "name": "users_tg", "u": "alice@acme.com", "p": "PASSWORD" } ``` **Answer** ```json { "success": true, "statusCode": 200, "data": { "token": "eyJ…", "refresh_token": "eyJ…", "expires_in": 259200 } } ``` - The app sends the token in `x-am-user-authorization`, next to `x-am-authorization` of the API user. The sample custom API `/default/login` of a new account gets both in one call. - The person may call an API when a group of theirs grants it **and** a group of the API user grants it. - The token holds the columns of the person the provider reads (a column called `name` included, as the person has it), the password fingerprint or the password changed at value, and two fields of its own : `__amTokenGenerator`, the provider which made it, and `__amAdminUserId`, the account of that provider. A refresh needs the `refresh_token` of the same provider. ## The Fields Generator - The **Fields Generator** tab of a DB provider is a function which returns extra fields to put in the token, from the row of the person (`g.req.body`) : a display name, a department… They come back in `g.req.auth.authAMDB` on every call without a lookup. - It runs in the sandbox, or on the native process with `runOnNativeProcess: true` (a **Native** chip on the list ; use `g.logger` there, not `console.log`). ## Custom providers - A **Custom** provider is your own logic : a **token generator** function which answers the token API for `{ "name": "", … }` with whatever it returns, and a **token validator** function which receives `g.req.body.token` and returns a truthy value (or an object) when the token is valid. Calls send that token in `x-custom-authorization` and your code reads the result in `g.req.auth.authCustom`. [Example](https://docs.apimaker.dev/v4/examples/req/auth/authCustom.html). ## Related - [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html) · [API group permissions](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) · [Multi-tenant users](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html#users-of-a-tenant) · [Custom API example](https://docs.apimaker.dev/v4/examples/req/auth/auth.html#database-user) --- # Auth of an API User > The first gate of every call - the token of an API user in x-am-authorization, how to get and refresh it, and what g.req.auth.authAMUser gives your code. Source: https://docs.apimaker.dev/v4/docs/authorization/AMApiUser.html Every non-public API needs the token of an [API user](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) : the application calling. It is the first of the [two gates](https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.html#the-two-gates). | | | |---|---| | Header | `x-am-authorization: ` | | Get it | `POST /api/system-api//token` with `{ "u", "p", "expiresInSeconds"? }` | | Answer | `{ "token", "refresh_token", "expires_in" }` | | In code | `g.req.auth.authAMUser` : the API user, with its name and groups. | **Get the token** ```json { "u": "default", "p": "12345", "expiresInSeconds": 259200 } ``` **TypeScript** **Who is calling** ```typescript const apiUser = g.req.auth.authAMUser; g.logger.log(apiUser.name, apiUser.groups); ``` - `expiresInSeconds` is optional : **Tokens Expire After** of the [Deployment Settings](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.html#security-apis) otherwise, 72 hours by default. Refresh with `{ "refresh_token" }` while the refresh token is valid (`refreshTokenValidForS` after the token expired, 900 seconds by default). - The groups of the API user decide which APIs, tables and fields the application reaches. A person token, when the API asks for one, narrows it further. ## Related - [Get token API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html) · [Auth of a database user](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) · [Handle role based permissions](https://docs.apimaker.dev/v4/docs/authorization/handle-role-based-permissions.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/authorization/AMApiUser.md) --- # Secrets > The secret of an API Maker account - hashing and encryption keys, the transfer key shared with apps, API user passwords, connection strings, default auth providers, multi-tenant entries - a TypeScript object kept encrypted on the server and never in Git. Source: https://docs.apimaker.dev/v4/docs/secrets/secrets.html A secret holds what must never be in the code : keys, passwords, connection strings. Every account gets one at creation, the **default secret**, and can add more. Code reads them with [get secret](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-secret-by-name-api.html) ; instances pick their connection string from them ; conversions and system APIs use their keys. | | | |---|---| | Page | `Security → Secret Management` : the secrets of the account, one of them the default. | | Shape | A TypeScript file which exports an object. `common` is read by API Maker ; add your own sections next to it. | | Kept | Encrypted on the server of this environment. Never pushed to Git : each environment has its own values. | | Another secret | Every request uses the default secret. Your code reads another secret of the account by its name : `getSecret(keys, fromSecretName)`. | ## The default secret of a new account ```typescript import * as T from 'types'; let Secret: T.ISecretType | any = { common: { hashingAlgorithm: 'SHA256', // the hashing conversion and the hash data API nonce: '85491cec-…', // key of the hashes ; without it, secret is used encryptionAlgorithm: 'AES', // the encryption conversion of the schemas secret: '1eaaca38-…', // never change it once data is encrypted with it encryptionAlgorithmFETransfer: 'AES', // encrypt and decrypt data system APIs secretFETransfer: '78e493ee-…', // shared with your frontend or mobile app feTransferDataValidityInSeconds: 300, // an encrypted payload older than this is refused apiUserPasswords: { // API users can read their password from a path of the secret default: '12345', }, connectionString: { // offered when you add an instance mongodb: 'mongodb://your_username:your_password@server_ip:27017/?authSource=admin&replicaSet=rs0&directConnection=true', mysql_8: 'mysql://your_username:your_password@server_ip:server_port?multipleStatements=true', mariadb: 'mariadb://your_username:your_password@server_ip:server_port?multipleStatements=true', sqlServer: 'Server=server_ip;User Id=user_id;Password=your_password;Trusted_Connection=True;TrustServerCertificate=True;', postgresql: 'postgresql://your_username:your_password@server_ip:server_port', oracle: 'server_ip:server_port/oracle_process_name', oracle_username: 'your_username', oracle_password: 'your_password', }, // authProviders: [], // the auth providers every API needs unless its settings say otherwise }, }; module.exports = Secret; ``` ## The keys of `common` | Key | Used by | |---|---| | `hashingAlgorithm`, `nonce` | The `hashing` conversion of a [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html#3-conversions-clean-the-value-first) and the [hash data](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-hash-data-api.html) API. `SHA256` is the algorithm supported. Without `nonce`, `secret` is the key. | | `encryptionAlgorithm`, `secret` | The `encryption` conversion : `AES`. `RC4` and `TRIPLEDES` are legacy ciphers, kept to read the data of API Maker 3 : do not choose them for new data. Changing `secret` means re-encrypting every encrypted value, and re-hashing when there was no nonce. | | `encryptionAlgorithmFETransfer`, `secretFETransfer` | The [encrypt](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-encrypt-data-api.html) and [decrypt](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-decrypt-data-api.html) APIs, the [encrypted payloads](https://docs.apimaker.dev/v4/docs/features/security-features.html#encrypted-request-payloads) and the encrypted answers. This is the key you give to your apps. | | `feTransferDataValidityInSeconds` | How old an encrypted payload may be. | | `apiUserPasswords` | Passwords an [API user](https://docs.apimaker.dev/v4/docs/apis-security/api-user-permission.html) reads by path (`common.apiUserPasswords.default`), so the password is not in Git. | | `connectionString` | The list offered by the instance form. Any key of the secret can hold one ; this section is a convention. | | `authProviders` | The names of the [auth providers](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) every API needs by default. Absent : only the API user token. The settings of an API, a table or a database override it. | | `multiTenant` | The tenants tables of [multi-tenant](https://docs.apimaker.dev/v4/docs/features/multi-tenant.html#2-point-the-default-secret-to-that-table) instances. | ## Your own keys **TypeScript** **Read them from code** ```typescript const apiKey = await g.sys.system.getSecret('stripe.apiKey'); const [ key, cs ] = await g.sys.system.getSecret([ 'stripe.apiKey', 'common.connectionString.mysql_8' ]); ``` - Add sections freely : `stripe: { apiKey: '…' }`. A path with dots reaches any key. - A team keeps one secret per environment with the same keys and other values : the code does not change between a laptop and production. ## Related - [Security features](https://docs.apimaker.dev/v4/docs/features/security-features.html) · [Connection strings](https://docs.apimaker.dev/v4/docs/Database-connection-string/mongodb-connection-strings.html) · [Get secret API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-get-secret-by-name-api.html) · [Git integration](https://docs.apimaker.dev/v4/docs/Git/git.html) (The code of this page is also written in Python, PHP, C#, Go, Rust, Java, Kotlin : https://docs.apimaker.dev/v4/docs/secrets/secrets.md) --- # Security Features > A security report of your account, encrypted payloads with a replay window, allowed origins, two-factor sign-in for the admin panel, and audits of the npm packages of your sandboxes and of the crates API Maker is built from. Source: https://docs.apimaker.dev/v4/docs/features/security-features.html - Tokens and groups decide who can call which API, table and field. The features below add more layers on top of them. ## APIs Security Report & Actions - **Security › APIs Security Report & Actions** scans the account and shows where a person could read or change rows that are not theirs: collections no pre-hook limits to the caller, groups that reach too far, weak auth providers and public APIs. - A leak comes with the request that proves it, and many findings come with an action that fixes them in one click, such as a row scoping pre-hook. Findings you accept are skipped with a reason. - The report gets a score out of 100 and is kept in git as `src/api_security_report/`, where an AI assistant working on the project can read it. - Learn more [APIs Security Report & Actions](https://docs.apimaker.dev/v4/docs/apis-security/api-security-report.html). ## Encrypted request payloads - Send the header `x-am-encrypted-payload: true` and put the encrypted payload in `dataEncFE`. - `dataEncFE` is `{ data, createdAt }` encrypted with `encryptionAlgorithmFETransfer` and `secretFETransfer` of the secret. Share that key with your frontend or mobile app. - **What it protects, and what it does not** : anyone who opens your app or reads its JavaScript can find that key, so it is no secret from your users. The format is the one of CryptoJS (`U2FsdGVkX1…`), whose key derivation is based on MD5. Use it as an extra layer when TLS ends at a proxy or a CDN you do not control, never in place of HTTPS, tokens and groups. - A payload older than `feTransferDataValidityInSeconds` is refused, so a captured request can not be replayed later. - Turn on `acceptOnlyEncryptedData` and plain bodies and query strings are refused. It can be set for a database, a table, an API, and for custom and system APIs. ```text POST /api/schema/admin/mongodb/shop/orders/save-single-or-multiple x-am-authorization: x-am-encrypted-payload: true { "dataEncFE": "U2FsdGVkX1+q3n..." } ``` ```typescript common: { encryptionAlgorithmFETransfer: 'AES', secretFETransfer: '...', feTransferDataValidityInSeconds: 300, // older payloads are refused }, ``` ## Encrypted responses - Send the header `x-am-get-encrypted-data` to get the response encrypted in `encryptedData`, with the same transfer key. | Value | Response | |---|---| | `no_encryption` | `data` only | | `get_only_encryption` | `encryptedData` only, `data` is null | | `get_data_and_encryption` | `data` and `encryptedData` | ## Allowed origins - Add the web origins of your apps in **Allowed Origins** on the Sandbox Settings page. - When the list has entries, a browser request from any other origin is refused with 403 before anything runs. - An empty list allows every origin. ## Two-factor sign-in - The root user can ask for a second factor when people sign in to the admin panel: a code from an authenticator app, a code sent by email, or both. - Recovery codes are shown once and stored hashed. - The root user also sets the code length and expiry, the attempts and the resend delay. - Email codes need the SMTP settings of the root user settings. ## Vulnerabilities - `Security → Vulnerabilities` audits two scopes for known vulnerabilities : a summary by severity (critical, high, moderate…), then each package with its range, how it arrives (the chains from a package asked for by name), the version which clears it and whether that version was checked, and the details of every advisory. - **Sandbox** : the packages of your sandboxes, of every language your code is written in. A chip per language shows how many of its packages are vulnerable and narrows the list to them ; every package says its language, and its link opens the registry it comes from. | Language | Registry | What is audited | Advisories | |---|---|---|---| | TypeScript / JavaScript | npm | The packages of Sandbox settings, resolved from the registry as an install would resolve them today, with everything they bring | npm | | Python | PyPI | What the image of its sandboxes has installed : the packages of Sandbox settings, what they brought, and the tools of the image | [OSV](https://osv.dev) | | PHP | Packagist | The same, from `composer.lock` of the image | OSV | | C# | NuGet | The same, from what NuGet restored in the image | OSV | | Go | Go modules | The same, from the modules of the build of the image | OSV | | Rust | crates.io | The same, from `Cargo.lock` of the image (with the crates of the runner of API Maker) | OSV (RustSec) | | Java, Kotlin | Maven Central | The same, from the tree Maven resolved in the image : the packages asked for and what each brought | OSV | - **The image says what runs.** For Python, PHP, C#, Go, Rust, Java and Kotlin the audit reads the packages in the image of the sandboxes : the version which is installed, not the one written in the settings (`time` `0.1.43` of Rust installs the newest `0.1.x`), and the packages a package brought with it (`requests` brings `idna` and `urllib3`). - **Before the image is built** (the language was never run on this server, or its build failed) only the packages named in the settings are audited, at the version named or the newest one, and the page says so. Composer refuses to install a version with known advisories : such an image is not built, and the audit still shows what is wrong with the package. - **What to do** is said per package : *set it to version x* for a package you added (in Sandbox settings), *rebuild the image* for a package another one brought, *a newer version of the runtime or an API Maker update* for what came with the image. **Compare versions** shows what each newer version of a package asked for by name would bring ; for a package of another language than JavaScript it compares the advisories of each version itself. - **API Maker** : what the server is made of. API Maker v4 is built from crates of Rust : the crates compiled into it are audited against [OSV](https://osv.dev) (the RustSec database), and their fixed versions read from crates.io. They change with an update of API Maker : [Deploy API Maker](https://docs.apimaker.dev/v4/docs/features/deploy-api-maker.html). - An audit is kept for an hour; an older one (up to 7 days) is shown at once while a new one runs, and **Check now** asks the registries again. Only package names and versions leave the server. ## Encrypted and hashed fields - Schema conversions can store a field encrypted or as an HMAC SHA-256 hash, with the keys of your secret. ## Good to know - API Maker itself speaks plain HTTP. Run it behind Caddy or another proxy that serves HTTPS: the installer sets up Caddy. - Encrypted payloads do not replace HTTPS. They add a layer on top of it, useful when TLS ends before API Maker.