Built-In and Custom Roles

A privilege pairs a resource (a database, a collection, the cluster) with actions on it — find, insert, dropCollection. A role bundles privileges and, optionally, roles it inherits; users hold roles, never privileges directly. The built-in roles cover most needs, each scoped to one database: read grants find, readWrite adds writes and index creation, dbAdmin grants indexes, stats and collMod but no document reads, and userAdmin manages users and roles; each has an AnyDatabase twin in admin. clusterMonitor suits a monitoring agent, clusterManager and hostManager cover reconfiguration and shutdown, and treating root as the default is the most common misconfiguration.

When readWrite is too wide — a reporting service that must read orders, append to an audit trail and touch nothing else — define the privileges yourself: custom roles can name individual collections.

A custom role scoped to two collections, and what it permitsJavaScript
const shop = db.getSiblingDB('shop');
shop.createRole({ role: 'orderReader', roles: [], privileges: [
  { resource: { db: 'shop', collection: 'orders' }, actions: ['find', 'listIndexes'] },
  { resource: { db: 'shop', collection: 'audit' }, actions: ['find', 'insert'] } ] });
shop.createUser({ user: 'report_svc', pwd: 'Str0ng-Rep-Pass',
                  roles: [{ role: 'orderReader', db: 'shop' }] });   // then reconnect
const t = (l, fn) => { try { print(l, fn()); } catch (e) { print(l, e.codeName); } };
t('find orders    ->', () => db.orders.countDocuments());
t('insert orders  ->', () => db.orders.insertOne({ _id: 'x' }).acknowledged);
t('insert audit   ->', () => db.audit.insertOne({ at: new Date() }).acknowledged);
t('read  other db ->', () => db.getSiblingDB('shop_restored').orders.countDocuments());
Output
find orders    -> 5
insert orders  -> Unauthorized
insert audit   -> true
read  other db -> Unauthorized

Insert is allowed on audit and refused on orders from a single role, because privileges are per collection. The full error text is worth reading once: the server echoes the whole command it rejected, naming the exact action to grant if the denial was wrong. To audit a user, ask for the flattened set rather than reading role definitions by hand: usersInfo with showPrivileges: true returns inheritedPrivileges across every nested role. Give every service its own user and role — one shared readWriteAnyDatabase user turns an injection bug into total loss.