Implementing Field-level Security
1. Protecting Sensitive Fields
| Field | Protection |
|---|---|
| passwordHash | Never expose |
| email / phone | Owner + admin only |
| ssn / payment | Encrypt at rest, mask by default |
| internalNotes | Role-restricted |
2. Using Shield Directives
Example: graphql-shield rule tree
import { shield, rule, and } from "graphql-shield";
const isAuth = rule()((_, __, ctx) => !!ctx.user);
const isOwner = rule()((p, _, ctx) => p.authorId === ctx.user?.id);
const permissions = shield({
Mutation: { updatePost: and(isAuth, isOwner) },
User: { email: isAuth }
});
3. Implementing Field Middleware
| Library | Detail |
|---|---|
| graphql-middleware | Wrap resolvers with before/after hooks |
| Envelop plugins | Plugin model for Yoga/v4 |
| Apollo plugins | willResolveField hook |
4. Masking Field Values
Example: Mask credit card
PaymentMethod: {
cardNumber: (pm, _, ctx) => {
if (!ctx.user.canSeeFullPan(pm)) return "**** **** **** " + pm.last4;
return pm.cardNumber;
}
}
5. Conditional Field Resolution
| Condition | Detail |
|---|---|
| Permission | Resolve only if user has permission |
| Feature flag | Hide behind LaunchDarkly/Unleash flag |
| Tenant config | Per-org enablement |
6. Auditing Field Access
Example: Audit log on sensitive read
User: {
ssn: (u, _, ctx) => {
ctx.audit.log({ actor: ctx.user.id, target: u.id, field: "ssn", op: "READ" });
return u.ssn;
}
}
7. Rate Limiting Fields
| Strategy | Detail |
|---|---|
| @rateLimit directive | Per-field cap (graphql-rate-limit) |
| Token bucket | N requests per time window per user |
| Sensitive ops | Lower limit on login, refund, etc. |
8. Implementing Cost Analysis
Example: graphql-cost-analysis
import costAnalysis from "graphql-cost-analysis";
validationRules: [costAnalysis({
maximumCost: 1000,
defaultCost: 1,
variables: req.body.variables,
createError: (max, actual) => new GraphQLError(`Query too expensive: ${actual} > ${max}`)
})]
9. Hiding Fields by Role
| Approach | Detail |
|---|---|
| Resolver returns null | Field appears in schema but value masked |
| Schema filtering | Build per-role schema (introspection-safe) |
| @auth directive | Throws if missing role |
10. Validating Field Arguments
Example: Argument validation
posts: (_, args) => {
if (args.first > 100) throw new GraphQLError("first must be ≤ 100", { extensions: { code: "BAD_USER_INPUT" } });
if (args.first < 1) throw new GraphQLError("first must be ≥ 1", { extensions: { code: "BAD_USER_INPUT" } });
return db.post.findMany({ take: args.first });
}