Data Access Controls
Document ID: ACP-001.3 Version: 1.0 Classification: Internal / Regulator-Shareable Effective Date: March 2026 Next Review: September 2026 Parent Document: ACP-001 — Access Control & Permissions
1. Overview
Beyond role-based permissions, Keshless enforces data-level access controls that ensure users can only access data they are authorized to see. This document covers ownership-based access, tenant isolation, KYC-gated operations, wallet security, and audit logging.
2. Ownership-Based Access
2.1 Principle
End users can only access their own data. Admin users can access any user's data (subject to their role permissions). This is enforced by the checkOwnershipOrAdmin() middleware.
2.2 Ownership Check Logic
Request to access resource with ID = :id
│
├── Is requester ADMIN, SUPER_ADMIN, or ADMIN_EMPLOYEE?
│ └── Yes → Access granted (subject to role permissions)
│
└── Does requester's userId match the resource owner?
├── Yes → Access granted
└── No → 403 Forbidden2.3 Implementation
The checkOwnershipOrAdmin(paramName) middleware extracts the resource identifier from the route parameter and compares it to the authenticated user's ID. This pattern is applied to:
- User profile endpoints (
/api/users/:id) - Transaction history (
/api/users/:id/transactions) - Wallet operations (
/api/wallets/:userId) - Verification status (
/api/verifications/:userId)
3. Vendor Data Isolation
3.1 Vendor Scoping
Vendor data is strictly isolated — each vendor can only access their own business data:
| Data Type | Scoping Mechanism |
|---|---|
| Transactions | Filtered by vendorId from JWT |
| Sub-users | Only sub-users belonging to parent vendor |
| Financial reports | Aggregated per-vendor |
| Customer data | Only customers who transacted with the vendor |
| Integration API keys | Scoped to the vendor that created them |
3.2 Vendor Sub-User Scoping
Vendor sub-users inherit their parent vendor's data scope but may be further restricted by their permission set:
- A cashier can create transactions but cannot view all transaction history
- An accountant can view financial reports but cannot create transactions
- A manager has broad access but cannot manage the vendor profile
3.3 Enforcement
The requireOwnVendor() middleware ensures that vendor API calls can only access the vendor's own data by comparing req.user.vendorId from the JWT against the requested vendor ID in the route or request body.
4. Service Center Isolation
4.1 Binding Model
Service center staff are bound to a specific service center via the serviceCenterId field on their AdminEmployee record. This binding is permanent (cannot be changed without admin intervention) and enforced at every request.
4.2 Scoping Rules
| Rule | Enforcement |
|---|---|
| Staff can only see their own SC's balance | req.serviceCenterId filters all queries |
| Staff can only process transactions at their SC | Transaction records tagged with serviceCenterId |
| SC Manager can only manage staff at their SC | Agent CRUD operations scoped by serviceCenterId |
| Inactive SC blocks all operations | requireServiceCenterStaff() checks SC status = ACTIVE |
4.3 Service Center Middleware
The requireServiceCenterStaff() middleware performs two checks:
- Staff assignment: Confirms the admin employee has a
serviceCenterIdassigned - SC active status: Verifies the service center record has status
ACTIVE
If either check fails, the request is rejected with a 403 response.
The attachServiceCenterIfStaff() middleware is a softer variant — it attaches req.serviceCenterId if the user is SC staff but does not reject non-SC users. This is used on shared endpoints accessible by both SC staff and regular admins.
5. KYC-Gated Access
5.1 Verification Status Model
User access to financial operations is gated by their KYC verification status:
| Status | Description | Access Level |
|---|---|---|
PENDING | Initial state, no KYC submitted | Basic account, limited operations |
IN_PROGRESS | KYC documents submitted, awaiting review | Same as PENDING |
VERIFIED | KYC approved | Full access to all wallet operations |
REJECTED | KYC failed | Restricted — must resubmit |
NEEDS_REVIEW | Flagged for manual compliance review | Operations may be suspended |
5.2 Operation Gating
| Operation | Requires KYC VERIFIED |
|---|---|
| Receive money | — |
| View balance | — |
| Send money (P2P) | ✓ |
| Withdraw cash | ✓ |
| Pay bills | ✓ |
| NFC payments | ✓ |
| Higher transaction limits | ✓ |
5.3 Admin KYC Permissions
Only admins with specific permissions can modify KYC status:
| Action | Required Permission |
|---|---|
| View KYC submissions | VIEW_VERIFICATIONS |
| Approve KYC | APPROVE_VERIFICATIONS |
| Reject KYC | REJECT_VERIFICATIONS |
| Service center KYC verification | SC_VERIFY_USER_KYC |
6. Wallet Security
6.1 Dual Balance Model
Every user wallet has two balance fields:
| Balance | Description | Usage |
|---|---|---|
walletBalance (withdrawable) | Funds that can be withdrawn to cash | Regular deposits, P2P receives |
nonWithdrawableBalance | Funds restricted from withdrawal | Promotional credits, refunds with conditions |
6.2 Cascade Deduction
When a user makes a payment, balances are deducted in order:
- Withdrawable balance deducted first
- Non-withdrawable balance used only if withdrawable is insufficient
- If combined balance is insufficient → transaction rejected
6.3 Wallet Access Rules
| Operation | Who Can Perform | Permission Required |
|---|---|---|
| View own balance | User | Authentication only |
| View any user's balance | Admin | MANAGE_USER_WALLETS |
| Manual wallet adjustment | Admin | MANUAL_ADJUSTMENTS |
| Process withdrawal | Admin | PROCESS_WITHDRAWALS |
| Approve withdrawal | Admin | APPROVE_WITHDRAWALS |
| SC user topup | SC Staff | SC_TOPUP_USER |
| SC user withdrawal | SC Staff | SC_WITHDRAW_USER |
| Vendor topup user | Vendor | topup_user (vendor permission) |
| Vendor withdraw user | Vendor | withdraw_user (vendor permission) |
6.4 Transaction Limits
Transaction limits are configured per context and enforced at the API level:
| Limit Type | Configured By | Applies To |
|---|---|---|
| Per-transaction max | System config | All users |
| Daily transaction limit | System config | Per user |
| Per-role limits | Admin config | By user role/KYC level |
| SC agent limits | SC Manager | Per service center agent |
| USSD limits | Environment config | USSD channel specifically |
USSD Configured Limits (from environment):
| Parameter | Default Value |
|---|---|
USSD_MAX_TRANSFER | E 10,000 |
USSD_MAX_WITHDRAWAL | E 5,000 |
USSD_MAX_AIRTIME | E 500 |
USSD_DAILY_LIMIT | E 50,000 |
7. Data Classification & Sensitive Field Handling
7.1 Data Classification
| Classification | Examples | Access Level |
|---|---|---|
| Restricted | National ID numbers, PINs, passwords, KYC images | Encrypted; accessed only by authorized services |
| Confidential | Wallet balances, transaction history, phone numbers | Authenticated access; scoped by ownership |
| Internal | System logs, audit trails, configuration | Admin access with appropriate permissions |
| Public | App store listings, marketing content | No access control |
7.2 Sensitive Field Masking
The following fields are masked in application logs and error responses:
- National ID numbers
- Dates of birth
- Passwords and PINs
- KYC document images
- Full wallet balances (shown only to authorized principals)
- OTP codes
7.3 KYC Document Storage
KYC documents (ID photos, selfies, proof of address) are stored in GCP Cloud Storage with the following access controls:
| Control | Implementation |
|---|---|
| Bucket access | Private (no public URLs) |
| Document access | Signed URLs with expiration |
| URL expiry | Time-limited (configurable) |
| Who can view | Admin with VIEW_VERIFICATIONS permission |
| Storage region | europe-west1 (data residency) |
8. Audit Trail
8.1 What Is Logged
All administrative actions and significant system events are captured in the audit log:
| Event Category | Examples |
|---|---|
| Authentication | Login, logout, failed login, password change |
| User management | Create, edit, block, delete user |
| Vendor management | Approve, block, edit vendor |
| Financial operations | Withdrawal processing, manual adjustment, reversal |
| KYC operations | Approve, reject verification |
| AML operations | Alert management, SAR filing |
| System changes | Emergency control activation, fee changes, limit changes |
| Admin management | Create, edit, delete admin employee |
8.2 Audit Log Fields
Each audit entry contains:
| Field | Description |
|---|---|
| Timestamp | ISO 8601 with timezone |
| Actor | User ID and role of who performed the action |
| Action | What was done (CREATE, UPDATE, DELETE, etc.) |
| Resource | What was affected (type and ID) |
| Changes | Before/after values for modifications |
| IP Address | Source IP of the request |
| Hash | SHA-256 hash for tamper detection |
8.3 Hash Chain Integrity
Audit log entries are chained using SHA-256 hashes. Each entry's hash includes the previous entry's hash, creating an append-only, tamper-evident log. If any entry is modified or deleted, the chain breaks and the tampering is detectable.
8.4 Audit Log Access
| Permission | Access Level |
|---|---|
VIEW_AUDIT_LOGS | Read audit trail entries |
| No permission exists to modify or delete audit logs | By design — audit logs are immutable |
Regulatory Significance: The immutable, hash-chained audit trail satisfies FATF Recommendation 11 (record keeping) and PCI DSS Requirement 10 (track and monitor access).
9. Database-Level Controls
9.1 Connection Security
| Control | Implementation |
|---|---|
| Cloud SQL Proxy | All DB connections via authenticated proxy |
| Private IP | Database not exposed to public internet |
| TLS | All connections encrypted in transit |
| Credentials | Stored in GCP Secret Manager |
| Region | europe-west1 (co-located with application) |
9.2 Application-Level ORM
All database queries go through Prisma ORM, which provides:
- Parameterized queries — prevents SQL injection
- Type-safe queries — compile-time validation
- Decimal precision —
Decimal(15,2)for all monetary values - Soft deletes — where applicable, records are marked deleted rather than removed