fix: stop sending accountId implicitly from the page client
The page client no longer resolves accountId from /v1/users/me. accountId is only sent when passed with --account-id, and a 403 for it now raises a clear error telling the user to omit it because the server scopes the listing to the caller's own account.
This commit is contained in:
@@ -41,3 +41,4 @@ Thumbs.db
|
|||||||
*.db
|
*.db
|
||||||
nowcast-point-alarm-filter.json
|
nowcast-point-alarm-filter.json
|
||||||
nowcast-point-*-backup-*
|
nowcast-point-*-backup-*
|
||||||
|
kayitlar-*.csv
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ Nokta alarm istemcileri `BE-NowcastPointAlarm` servisinin `/v1/alarms/points` en
|
|||||||
|
|
||||||
- **Page endpoint'inde sıralama (ORDER BY) yoktur.** Tüm sayfalar gezilirken kayıtların sırası garanti değildir; bu yüzden sayfa kayması yaşanabilir (bir kayıt iki kez gelebilir, bir kayıt hiç gelmeyebilir). Backend tarafında `createdAt` ardından `id` ile sıralama önerildi (yeni kayıtlar sona eklenir, `id` eşit zamanlarda sırayı sabitler).
|
- **Page endpoint'inde sıralama (ORDER BY) yoktur.** Tüm sayfalar gezilirken kayıtların sırası garanti değildir; bu yüzden sayfa kayması yaşanabilir (bir kayıt iki kez gelebilir, bir kayıt hiç gelmeyebilir). Backend tarafında `createdAt` ardından `id` ile sıralama önerildi (yeni kayıtlar sona eklenir, `id` eşit zamanlarda sırayı sabitler).
|
||||||
- **Tüm kayıtlar çekilirken aynı anda register, update veya unregister yapmayın.** Veri değişirse sayfalar kayar. `nowcast-point-page.py` sonunda sunucunun bildirdiği `totalRecords` ile alınan benzersiz kayıt sayısını karşılaştırır ve fark varsa uyarı verir; bu durumda komutu yeniden çalıştırın. SQLite `registrationId` ile upsert yaptığı için tekrar çalıştırmak kayıt çoğaltmaz.
|
- **Tüm kayıtlar çekilirken aynı anda register, update veya unregister yapmayın.** Veri değişirse sayfalar kayar. `nowcast-point-page.py` sonunda sunucunun bildirdiği `totalRecords` ile alınan benzersiz kayıt sayısını karşılaştırır ve fark varsa uyarı verir; bu durumda komutu yeniden çalıştırın. SQLite `registrationId` ile upsert yaptığı için tekrar çalıştırmak kayıt çoğaltmaz.
|
||||||
- `accountId` verilmezse ADMIN kullanıcılar tüm hesapları görür. Betik her zaman `/v1/users/me` içindeki `account.id` değerini gönderir (`--account-id` ile değiştirilebilir). Yetki yoksa (403) uyarı verip `accountId` olmadan bir kez daha dener.
|
- `nowcast-point-page.py` varsayılan olarak `accountId` göndermez; sunucu listeyi çağıranın kendi hesabına daraltır ve normal kullanıcılar için doğru kullanım budur. `--account-id` yalnızca en az POWER_USER yetkisiyle verilebilir; aksi halde HTTP 403 döner ve betik `--account-id` vermemenizi söyleyen bir hata yazar. ADMIN kullanıcı `accountId` vermezse tüm hesapları görür.
|
||||||
- Backend her `RuntimeException`'ı HTTP 400 yapar; kayıt bulunamaması da 400 döner. Yalnızca gerçek 404 `NOT_FOUND` olarak işaretlenir.
|
- Backend her `RuntimeException`'ı HTTP 400 yapar; kayıt bulunamaması da 400 döner. Yalnızca gerçek 404 `NOT_FOUND` olarak işaretlenir.
|
||||||
|
|
||||||
### page
|
### page
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ def parse_args() -> argparse.Namespace:
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--account-id",
|
"--account-id",
|
||||||
help="Sorgulanacak hesap ID'si (varsayılan: /v1/users/me içindeki account.id)",
|
help=(
|
||||||
|
"Sorgulanacak hesap ID'si; yalnızca en az POWER_USER yetkisiyle verilebilir (aksi halde "
|
||||||
|
"HTTP 403). Verilmezse sunucu kendi hesabınıza daraltır; normal kullanıcılar vermemelidir"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--recipient-id",
|
"--recipient-id",
|
||||||
@@ -95,19 +98,18 @@ def main() -> int:
|
|||||||
polygon = parse_polygon(args.polygon) if args.polygon else None
|
polygon = parse_polygon(args.polygon) if args.polygon else None
|
||||||
client = IklimClient.from_env(BASE_DIR / ".env")
|
client = IklimClient.from_env(BASE_DIR / ".env")
|
||||||
service = PointAlarmService(client)
|
service = PointAlarmService(client)
|
||||||
account_id = args.account_id or service.get_account_id()
|
|
||||||
store = PointStore(args.db)
|
store = PointStore(args.db)
|
||||||
summary = fetch_pages(
|
summary = fetch_pages(
|
||||||
service,
|
service,
|
||||||
store,
|
store,
|
||||||
account_id=account_id,
|
account_id=args.account_id,
|
||||||
recipient_ids=args.recipient_id or None,
|
recipient_ids=args.recipient_id or None,
|
||||||
polygon=polygon,
|
polygon=polygon,
|
||||||
page_size=args.page_size,
|
page_size=args.page_size,
|
||||||
page_number=args.page_number,
|
page_number=args.page_number,
|
||||||
)
|
)
|
||||||
records = store.records(summary.account_id, summary.registration_ids)
|
records = store.records(None, summary.registration_ids)
|
||||||
webhooks = store.webhooks(summary.account_id)
|
webhooks = store.webhooks_of(records)
|
||||||
if args.output:
|
if args.output:
|
||||||
path = timestamped_path(args.output)
|
path = timestamped_path(args.output)
|
||||||
write_records(path, records, webhooks)
|
write_records(path, records, webhooks)
|
||||||
@@ -125,7 +127,7 @@ def main() -> int:
|
|||||||
if summary.duplicates:
|
if summary.duplicates:
|
||||||
logging.warning("Sayfalar arasında %s tekrar eden kayıt görüldü.", summary.duplicates)
|
logging.warning("Sayfalar arasında %s tekrar eden kayıt görüldü.", summary.duplicates)
|
||||||
if not args.output:
|
if not args.output:
|
||||||
prompt_export(store, summary.account_id)
|
prompt_export(store, args.account_id)
|
||||||
except (ConfigurationError, ApiError, OSError) as exc:
|
except (ConfigurationError, ApiError, OSError) as exc:
|
||||||
logging.error("İşlem durduruldu: %s", exc)
|
logging.error("İşlem durduruldu: %s", exc)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
+13
-11
@@ -336,6 +336,11 @@ class PointStore:
|
|||||||
query += " ORDER BY recipientId, registrationId"
|
query += " ORDER BY recipientId, registrationId"
|
||||||
return [dict(row) for row in self.connection.execute(query, params)]
|
return [dict(row) for row in self.connection.execute(query, params)]
|
||||||
|
|
||||||
|
def webhooks_of(self, records: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
"""Webhooks of the accounts that own the given records."""
|
||||||
|
owners = sorted({record["accountId"] for record in records if record.get("accountId")})
|
||||||
|
return [webhook for owner in owners for webhook in self.webhooks(owner)]
|
||||||
|
|
||||||
def webhooks(self, account_id: str | None = None) -> list[dict[str, Any]]:
|
def webhooks(self, account_id: str | None = None) -> list[dict[str, Any]]:
|
||||||
if account_id:
|
if account_id:
|
||||||
rows = self.connection.execute(
|
rows = self.connection.execute(
|
||||||
@@ -432,8 +437,7 @@ def fetch_pages(
|
|||||||
) -> FetchSummary:
|
) -> FetchSummary:
|
||||||
"""Fetch one page (page_number given) or every page into the store.
|
"""Fetch one page (page_number given) or every page into the store.
|
||||||
|
|
||||||
A 403 for an explicit accountId (caller below POWER_USER) is retried once without
|
Without account_id the server scopes the listing to the caller's own account.
|
||||||
it; the server then scopes the listing to the caller's own account.
|
|
||||||
"""
|
"""
|
||||||
summary = FetchSummary(account_id=account_id)
|
summary = FetchSummary(account_id=account_id)
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
@@ -443,24 +447,22 @@ def fetch_pages(
|
|||||||
data = service.page(
|
data = service.page(
|
||||||
page_number=current,
|
page_number=current,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
account_id=summary.account_id,
|
account_id=account_id,
|
||||||
recipient_ids=recipient_ids,
|
recipient_ids=recipient_ids,
|
||||||
polygon=polygon,
|
polygon=polygon,
|
||||||
)
|
)
|
||||||
except ApiError as exc:
|
except ApiError as exc:
|
||||||
if exc.status_code == 403 and summary.account_id:
|
if exc.status_code == 403 and account_id:
|
||||||
LOGGER.warning(
|
raise ConfigurationError(
|
||||||
"accountId ile sorgu 403 verdi; accountId olmadan, sunucunun kendi hesabınıza "
|
"accountId parametresi için en az POWER_USER yetkisi gerekir (HTTP 403). "
|
||||||
"daraltmasıyla yeniden deneniyor."
|
"--account-id vermeyin; sistem hesabınızı kendisi tespit eder."
|
||||||
)
|
) from exc
|
||||||
summary.account_id = None
|
|
||||||
continue
|
|
||||||
raise
|
raise
|
||||||
registrations = [item for item in (data.get("registrations") or []) if isinstance(item, dict)]
|
registrations = [item for item in (data.get("registrations") or []) if isinstance(item, dict)]
|
||||||
summary.total_records = int(data.get("totalRecords") or 0)
|
summary.total_records = int(data.get("totalRecords") or 0)
|
||||||
summary.received += len(registrations)
|
summary.received += len(registrations)
|
||||||
for registration in registrations:
|
for registration in registrations:
|
||||||
registration_id = store.upsert_registration(registration, summary.account_id)
|
registration_id = store.upsert_registration(registration, account_id)
|
||||||
if registration_id and registration_id not in seen:
|
if registration_id and registration_id not in seen:
|
||||||
seen.add(registration_id)
|
seen.add(registration_id)
|
||||||
summary.registration_ids.append(registration_id)
|
summary.registration_ids.append(registration_id)
|
||||||
|
|||||||
@@ -97,11 +97,12 @@ class PointAlarmTest(unittest.TestCase):
|
|||||||
self.assertEqual(summary.received, 2)
|
self.assertEqual(summary.received, 2)
|
||||||
self.assertEqual(len(service.calls), 1)
|
self.assertEqual(len(service.calls), 1)
|
||||||
|
|
||||||
def test_forbidden_account_retries_without_it(self) -> None:
|
def test_forbidden_account_gives_clear_error(self) -> None:
|
||||||
service = FakeService([registration(1)], forbid_account=True)
|
service = FakeService([registration(1)], forbid_account=True)
|
||||||
summary = fetch_pages(service, self.store, account_id="acc")
|
with self.assertRaisesRegex(ConfigurationError, "--account-id vermeyin"):
|
||||||
|
fetch_pages(service, self.store, account_id="acc")
|
||||||
|
summary = fetch_pages(service, self.store, account_id=None)
|
||||||
self.assertEqual(summary.distinct, 1)
|
self.assertEqual(summary.distinct, 1)
|
||||||
self.assertIsNone(service.calls[-1]["account"])
|
|
||||||
|
|
||||||
def test_csv_and_txt_output(self) -> None:
|
def test_csv_and_txt_output(self) -> None:
|
||||||
fetch_pages(FakeService([registration(1)]), self.store, account_id="acc")
|
fetch_pages(FakeService([registration(1)]), self.store, account_id="acc")
|
||||||
|
|||||||
Reference in New Issue
Block a user