CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

MongoDB authentication logs #3732

ClosedLccantynz wants to mergecursor/mongodb-authentication-logs-b3c9mainopened Feb 15, 2026
11 changed files+409−30
Modified.env.example+7−1View fileUnifiedSplit
11# Database Configuration
2MONGO_URL=mongodb+srv://username:password@cluster.mongodb.net/?retryWrites=true&w=majority
2# Prefer `MONGO_URI`. `MONGO_URL` is still supported for backward compatibility.
3# IMPORTANT: this value is the *full MongoDB connection string (URI)*, not a username.
4# If Atlas auth logs show a username like `MONGO_URL`, your URI may be misconfigured
5# (e.g. `mongodb+srv://MONGO_URL:...@...`). Fix it and rotate DB credentials.
6MONGO_URI=mongodb+srv://db_user:db_password@cluster.mongodb.net/?retryWrites=true&w=majority
7# Legacy name (optional):
8# MONGO_URL=${MONGO_URI}
39DB_NAME=hibiscus_airport
410
511# Stripe Payment
ModifiedADMIN_LOGIN_FIX_GUIDE.md+3−2View fileUnifiedSplit
5353
5454### Backend (Set in Render Dashboard)
5555```
56MONGO_URL=mongodb+srv://...
56# Prefer MONGO_URI (full MongoDB connection string). MONGO_URL is still supported.
57MONGO_URI=mongodb+srv://...
5758DB_NAME=hibiscus_airport
5859ADMIN_EMAIL=bookings@bookaride.co.nz
5960FRONTEND_URL=https://hibiscustoairport.co.nz
134135## Troubleshooting
135136
136137### Issue: Login returns 401
137- Check that `MONGO_URL` and `DB_NAME` are set in Render
138- Check that `MONGO_URI` (or legacy `MONGO_URL`) and `DB_NAME` are set in Render
138139- Verify the database is accessible from Render
139140- Check Render logs for connection errors
140141
ModifiedBOOKARIDE_COMPLETE_HANDBOOK.md+2−1View fileUnifiedSplit
195195
196196```env
197197# Database
198MONGO_URL=mongodb://localhost:27017
198# Prefer `MONGO_URI` (full connection string). `MONGO_URL` is supported for backward compatibility.
199MONGO_URI=mongodb://localhost:27017
199200DB_NAME=your_database_name
200201
201202# Authentication
ModifiedBOOKARIDE_QUICK_REFERENCE.md+2−1View fileUnifiedSplit
1616## 2. BACKEND .env FILE
1717
1818```env
19MONGO_URL=mongodb://localhost:27017
19# Prefer `MONGO_URI` (full connection string). `MONGO_URL` is supported for backward compatibility.
20MONGO_URI=mongodb://localhost:27017
2021DB_NAME=bookaride
2122JWT_SECRET_KEY=your-secret-key
2223PUBLIC_DOMAIN=https://yourdomain.com
ModifiedDEPLOYMENT_STATUS.md+2−2View fileUnifiedSplit
8484Make sure these are set in Render dashboard:
8585
8686**Critical (Required):**
87- `MONGO_URL` - MongoDB connection string
87- `MONGO_URI` (or legacy `MONGO_URL`) - MongoDB connection string (full URI)
8888- `DB_NAME` - Database name (e.g., `hibiscus_airport`)
8989
9090**Important (For full functionality):**
130130
131131### If login still fails after deployment:
1321321. Check Render logs for errors
1332. Verify MongoDB connection (MONGO_URL is correct)
1332. Verify MongoDB connection string (MONGO_URI/MONGO_URL is correct)
1341343. Check that database is accessible from Render's IP
1351354. Verify environment variables are set
136136
Modifiedbackend/admin_routes.py+11−5View fileUnifiedSplit
1111
1212router = APIRouter()
1313
14# MongoDB config (supports MONGO_URI or MONGO_URL; redacts secrets in logs)
15try:
16 from mongo_config import get_mongo_uri, get_db_name
17except ImportError: # pragma: no cover
18 from backend.mongo_config import get_mongo_uri, get_db_name # type: ignore
19
1420ADMIN_COOKIE = "d8_admin"
1521ADMIN_API_KEY = os.environ.get("ADMIN_API_KEY", "").strip()
1622
288294 return JSONResponse({"ok": False, "error": "unauthorized"}, status_code=401)
289295 try:
290296 from motor.motor_asyncio import AsyncIOMotorClient
291 mongo_url = os.environ.get("MONGO_URL", "")
292 db_name = os.environ.get("DB_NAME", "hibiscus_shuttle")
293 if not mongo_url:
294 return JSONResponse({"ok": False, "error": "MONGO_URL not set", "items": []})
295 client = AsyncIOMotorClient(mongo_url)
297 mongo_uri = get_mongo_uri(required=False)
298 db_name = get_db_name("hibiscus_shuttle")
299 if not mongo_uri:
300 return JSONResponse({"ok": False, "error": "MONGO_URI/MONGO_URL not set", "items": []})
301 client = AsyncIOMotorClient(mongo_uri)
296302 db = client[db_name]
297303 docs = await db.bookings.find({}, {"_id": 0}).sort("createdAt", -1).to_list(500)
298304 client.close()
Modifiedbackend/booking_routes.py+12−8View fileUnifiedSplit
1515from motor.motor_asyncio import AsyncIOMotorClient
1616from auth import get_current_user, verify_password, create_access_token, get_password_hash
1717
18# MongoDB config (supports MONGO_URI or MONGO_URL; redacts secrets in logs)
19try:
20 from mongo_config import get_mongo_uri, get_db_name
21except ImportError: # pragma: no cover
22 from backend.mongo_config import get_mongo_uri, get_db_name # type: ignore
23
1824# Load environment variables
1925ROOT_DIR = Path(__file__).parent
2026load_dotenv(ROOT_DIR / '.env')
4248router = APIRouter()
4349
4450# MongoDB connection
45mongo_url = os.environ['MONGO_URL']
46client = AsyncIOMotorClient(mongo_url)
47db = client[os.environ['DB_NAME']]
51mongo_uri = get_mongo_uri()
52client = AsyncIOMotorClient(mongo_uri)
53db = client[get_db_name()]
4854
4955# Stripe setup
5056stripe.api_key = os.environ.get('STRIPE_SECRET_KEY')
35503556 password: str
35513557
35523558def _get_db():
3553 mongo_url = (os.getenv("MONGO_URL") or "").strip()
3554 db_name = (os.getenv("DB_NAME") or "hibiscustoairport").strip()
3555 if not mongo_url:
3556 raise RuntimeError("MONGO_URL is not set")
3557 client = MongoClient(mongo_url)
3559 mongo_uri = get_mongo_uri()
3560 db_name = get_db_name("hibiscustoairport")
3561 client = MongoClient(mongo_uri)
35583562 return client[db_name]
35593563
35603564@router.post("/admin/bootstrap")
Modifiedbackend/database.py+5−5View fileUnifiedSplit
1import os
21from pymongo import MongoClient
32
4MONGO_URL = os.getenv("MONGO_URL")
5if not MONGO_URL:
6 raise RuntimeError("MONGO_URL env var not set")
3try:
4 from mongo_config import get_mongo_uri
5except ImportError: # pragma: no cover
6 from backend.mongo_config import get_mongo_uri # type: ignore
77
8client = MongoClient(MONGO_URL)
8client = MongoClient(get_mongo_uri())
99db = client.get_default_database()
Addedbackend/mongo_config.py+98−0View fileUnifiedSplit
1import logging
2import os
3import re
4from typing import List, Optional
5from urllib.parse import urlsplit, urlunsplit
6
7logger = logging.getLogger(__name__)
8
9
10def redact_mongo_uri(uri: str) -> str:
11 """
12 Redact password in MongoDB URIs for safe logging.
13
14 Examples:
15 mongodb+srv://user:pass@cluster/db -> mongodb+srv://user:***@cluster/db
16 mongodb://user@host/db -> unchanged (no password present)
17 """
18 if not uri:
19 return uri
20 try:
21 parts = urlsplit(uri)
22 netloc = parts.netloc or ""
23 if "@" not in netloc:
24 return uri
25
26 userinfo, hostinfo = netloc.rsplit("@", 1)
27 if ":" in userinfo:
28 user, _pwd = userinfo.split(":", 1)
29 userinfo = f"{user}:***"
30 # else: username-only userinfo, nothing to redact
31 redacted = urlunsplit((parts.scheme, f"{userinfo}@{hostinfo}", parts.path, parts.query, parts.fragment))
32 return redacted
33 except Exception:
34 # Best-effort regex fallback
35 return re.sub(r"://([^:@/]+):([^@/]+)@", r"://\1:***@", uri)
36
37
38def _mongo_uri_warnings(uri: str) -> List[str]:
39 warnings: List[str] = []
40 if not uri:
41 warnings.append("MongoDB URI is empty")
42 return warnings
43
44 if not (uri.startswith("mongodb://") or uri.startswith("mongodb+srv://")):
45 warnings.append("MongoDB URI does not start with mongodb:// or mongodb+srv://")
46
47 # Common placeholder patterns in docs/snippets.
48 if any(tok in uri for tok in ("<username>", "<password>", "username:password@", "user:pass@")):
49 warnings.append("MongoDB URI appears to contain placeholder credentials (e.g. username:password@)")
50
51 try:
52 parts = urlsplit(uri)
53 username = parts.username
54 password = parts.password
55 except Exception:
56 username = None
57 password = None
58
59 bad_usernames = {"MONGO_URL", "MONGO_URI", "username", "user", "admin", "root"}
60 bad_passwords = {"password", "pass", "changeme", "change-me", "123456", "admin"}
61
62 if username and username in bad_usernames:
63 warnings.append(
64 f"MongoDB URI username is '{username}' (common misconfiguration / placeholder). "
65 "If this matches Atlas auth logs, fix the connection string and rotate DB creds."
66 )
67 if password and password in bad_passwords:
68 warnings.append("MongoDB URI password looks like a placeholder/weak password — rotate credentials.")
69
70 return warnings
71
72
73def get_mongo_uri(*, required: bool = True, log_warnings: bool = True) -> str:
74 """
75 Prefer MONGO_URI, fallback to MONGO_URL for backward compatibility.
76 """
77 uri = (os.getenv("MONGO_URI") or os.getenv("MONGO_URL") or "").strip()
78 if not uri:
79 if required:
80 raise RuntimeError("MongoDB connection string missing (set MONGO_URI or MONGO_URL)")
81 return ""
82
83 warnings = _mongo_uri_warnings(uri)
84 strict = (os.getenv("MONGO_URI_STRICT") or "").strip().lower() in {"1", "true", "yes", "on"}
85 if warnings and strict:
86 joined = "; ".join(warnings)
87 raise RuntimeError(f"Invalid/suspicious MongoDB URI configuration: {joined}. URI={redact_mongo_uri(uri)}")
88
89 if warnings and log_warnings:
90 for w in warnings:
91 logger.warning(f"[mongo_config] {w}. URI={redact_mongo_uri(uri)}")
92
93 return uri
94
95
96def get_db_name(default: str = "hibiscus_shuttle") -> str:
97 return (os.getenv("DB_NAME") or default).strip()
98
Modifiedbackend/server.py+11−5View fileUnifiedSplit
4444)
4545logger = logging.getLogger(__name__)
4646
47# MongoDB config (supports MONGO_URI or MONGO_URL; redacts secrets in logs)
48try:
49 from mongo_config import get_mongo_uri, get_db_name
50except ImportError: # pragma: no cover
51 from backend.mongo_config import get_mongo_uri, get_db_name # type: ignore
52
4753# ---------------------------------------------------------------------------
4854# Middleware: prevent Cloudflare/CDN caching of API responses
4955# ---------------------------------------------------------------------------
170176 """Send reminders for bookings happening tomorrow — runs daily at 6 PM NZ time."""
171177 try:
172178 logger.info("Running day-before reminder job...")
173 mongo_url = os.environ.get('MONGO_URL', '')
174 db_name = os.environ.get('DB_NAME', 'hibiscus_shuttle')
175 if not mongo_url:
176 logger.warning("MONGO_URL not set, skipping reminders")
179 mongo_uri = get_mongo_uri(required=False)
180 db_name = get_db_name("hibiscus_shuttle")
181 if not mongo_uri:
182 logger.warning("MONGO_URI/MONGO_URL not set, skipping reminders")
177183 return
178 client = AsyncIOMotorClient(mongo_url)
184 client = AsyncIOMotorClient(mongo_uri)
179185 db = client[db_name]
180186 tomorrow = (datetime.now(timezone.utc) + timedelta(days=1)).strftime('%Y-%m-%d')
181187 bookings = await db.bookings.find({
Addedtools/parse_atlas_auth_logs.py+256−0View fileUnifiedSplit
1#!/usr/bin/env python3
2"""
3Parse MongoDB Atlas "Authentication Logs" pasted from the UI (or exported text),
4and print a quick security-oriented summary.
5
6It handles the common paste format where each cell appears on its own line with
7blank lines between fields.
8"""
9
10from __future__ import annotations
11
12import argparse
13import csv
14import json
15import re
16import sys
17from collections import Counter
18from dataclasses import asdict, dataclass
19from datetime import datetime
20from pathlib import Path
21from typing import Iterable, List, Optional, Tuple
22
23
24HEADER_HINTS = (
25 "Timestamp",
26 "Username",
27 "IP Address",
28 "Host",
29 "Authentication Source",
30 "Authentication Result",
31)
32
33
34@dataclass(frozen=True)
35class AuthEvent:
36 timestamp_raw: str
37 username: str
38 ip: str
39 host: str
40 auth_source: str
41 result: str
42 timestamp_iso: Optional[str] = None
43
44
45def _parse_timestamp(ts: str) -> Optional[str]:
46 ts = ts.strip()
47 if not ts:
48 return None
49 # Example: "2/15/2026 - 11:03:35 PM"
50 for fmt in ("%m/%d/%Y - %I:%M:%S %p", "%m/%d/%Y %I:%M:%S %p"):
51 try:
52 dt = datetime.strptime(ts, fmt)
53 return dt.isoformat()
54 except ValueError:
55 continue
56 return None
57
58
59def _redact_ip(ip: str) -> str:
60 ip = ip.strip()
61 if not ip:
62 return ip
63 if ":" in ip: # IPv6-ish
64 # Keep only first 3 hextets
65 parts = ip.split(":")
66 return ":".join(parts[:3]) + ":…"
67 m = re.match(r"^(\d+)\.(\d+)\.(\d+)\.(\d+)$", ip)
68 if not m:
69 return ip
70 return f"{m.group(1)}.{m.group(2)}.{m.group(3)}.x"
71
72
73def _redact_username(u: str) -> str:
74 u = u.strip()
75 if not u:
76 return u
77 # X.509 subjects often look like "CN=email@domain"
78 if u.startswith("CN=") and "@" in u:
79 left, right = u.split("CN=", 1)
80 # right may include other subject parts, but for our common case it's email.
81 email = right.strip()
82 # Minimal email redaction: keep domain, shorten local part.
83 if "@" in email:
84 local, domain = email.split("@", 1)
85 if local:
86 local = local[0] + "…"
87 return f"CN={local}@{domain}"
88 return u
89
90
91def _clean_nonempty_lines(text: str) -> List[str]:
92 return [ln.strip() for ln in text.splitlines() if ln.strip()]
93
94
95def _drop_header(lines: List[str]) -> List[str]:
96 if not lines:
97 return lines
98 head = lines[0]
99 if all(h in head for h in ("Timestamp", "Username", "IP")):
100 return lines[1:]
101 return lines
102
103
104def _chunk(lines: List[str], size: int) -> List[List[str]]:
105 return [lines[i : i + size] for i in range(0, len(lines), size)]
106
107
108def parse_events(text: str) -> Tuple[List[AuthEvent], List[str]]:
109 lines = _drop_header(_clean_nonempty_lines(text))
110 errors: List[str] = []
111 if not lines:
112 return [], ["No rows found (input was empty after stripping whitespace)"]
113
114 chunks = _chunk(lines, 6)
115 events: List[AuthEvent] = []
116 for idx, c in enumerate(chunks):
117 if len(c) != 6:
118 errors.append(
119 f"Row {idx+1}: expected 6 fields (timestamp, username, ip, host, auth_source, result) "
120 f"but got {len(c)} fields. Remaining lines may be malformed."
121 )
122 continue
123 ts, user, ip, host, src, res = c
124 events.append(
125 AuthEvent(
126 timestamp_raw=ts,
127 username=user,
128 ip=ip,
129 host=host,
130 auth_source=src,
131 result=res,
132 timestamp_iso=_parse_timestamp(ts),
133 )
134 )
135 return events, errors
136
137
138def summarize(events: List[AuthEvent]) -> str:
139 if not events:
140 return "No events."
141
142 def k(e: AuthEvent) -> Tuple[str, str, str, str, str, str]:
143 return (e.timestamp_raw, e.username, e.ip, e.host, e.auth_source, e.result)
144
145 total = len(events)
146 uniq = len({k(e) for e in events})
147 dupes = total - uniq
148
149 by_user = Counter(e.username for e in events)
150 by_ip = Counter(e.ip for e in events)
151 by_source = Counter(e.auth_source for e in events)
152 by_result = Counter(e.result for e in events)
153
154 ts_known = [e.timestamp_iso for e in events if e.timestamp_iso]
155 ts_range = ""
156 if ts_known:
157 ts_range = f"{min(ts_known)} .. {max(ts_known)}"
158
159 suspicious_users = [u for u in by_user if u in {"MONGO_URL", "MONGO_URI", "username", "user"}]
160 warnings: List[str] = []
161 if suspicious_users:
162 warnings.append(
163 "Found suspicious/placeholder usernames in auth logs: "
164 + ", ".join(sorted(set(suspicious_users)))
165 + " (this often means the connection string username was set incorrectly)."
166 )
167 if any(e.auth_source == "admin" and e.username == "MONGO_URL" for e in events):
168 warnings.append("User 'MONGO_URL' authenticated against 'admin' — strongly check DB users/creds and rotate.")
169
170 lines: List[str] = []
171 lines.append(f"Total events: {total} (unique: {uniq}, duplicates: {dupes})")
172 if ts_range:
173 lines.append(f"Time range: {ts_range}")
174 lines.append(f"Results: {dict(by_result)}")
175 lines.append(f"Auth sources: {dict(by_source)}")
176 lines.append("Top usernames:")
177 for u, c in by_user.most_common(10):
178 lines.append(f" - {u}: {c}")
179 lines.append("Top IPs:")
180 for ip, c in by_ip.most_common(10):
181 lines.append(f" - {ip}: {c}")
182 if warnings:
183 lines.append("Warnings:")
184 for w in warnings:
185 lines.append(f" - {w}")
186 return "\n".join(lines)
187
188
189def main(argv: Optional[List[str]] = None) -> int:
190 p = argparse.ArgumentParser(description="Parse MongoDB Atlas Authentication Logs paste/export.")
191 p.add_argument("-i", "--input", help="Input text file (defaults to stdin)")
192 p.add_argument("--out-json", help="Write parsed events as JSON to this path")
193 p.add_argument("--out-csv", help="Write parsed events as CSV to this path")
194 p.add_argument("--no-redact", action="store_true", help="Do not redact usernames/IPs in outputs")
195 args = p.parse_args(argv)
196
197 if args.input:
198 text = Path(args.input).read_text(encoding="utf-8", errors="replace")
199 else:
200 text = sys.stdin.read()
201
202 events, errors = parse_events(text)
203
204 redact = not args.no_redact
205 out_events = events
206 if redact:
207 out_events = [
208 AuthEvent(
209 timestamp_raw=e.timestamp_raw,
210 username=_redact_username(e.username),
211 ip=_redact_ip(e.ip),
212 host=e.host,
213 auth_source=e.auth_source,
214 result=e.result,
215 timestamp_iso=e.timestamp_iso,
216 )
217 for e in events
218 ]
219
220 if args.out_json:
221 Path(args.out_json).write_text(
222 json.dumps([asdict(e) for e in out_events], indent=2, sort_keys=True) + "\n",
223 encoding="utf-8",
224 )
225
226 if args.out_csv:
227 with Path(args.out_csv).open("w", newline="", encoding="utf-8") as f:
228 w = csv.DictWriter(
229 f,
230 fieldnames=[
231 "timestamp_raw",
232 "timestamp_iso",
233 "username",
234 "ip",
235 "host",
236 "auth_source",
237 "result",
238 ],
239 )
240 w.writeheader()
241 for e in out_events:
242 w.writerow(asdict(e))
243
244 print(summarize(out_events))
245 if errors:
246 print("\nParse notes:", file=sys.stderr)
247 for e in errors:
248 print(f"- {e}", file=sys.stderr)
249
250 # Non-zero if we couldn't parse cleanly.
251 return 0 if not errors else 2
252
253
254if __name__ == "__main__":
255 raise SystemExit(main())
256
0257
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts