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.
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());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.