portal-mojo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +55 -0
- package/docs/DetailView.md +173 -0
- package/docs/ModelTable.md +128 -0
- package/docs/README.md +98 -0
- package/docs/admin-assistant.md +72 -0
- package/docs/admin-bouncer.md +120 -0
- package/docs/admin-cloudwatch.md +38 -0
- package/docs/admin-credentials.md +116 -0
- package/docs/admin-dashboard.md +15 -0
- package/docs/admin-devices-geoip.md +284 -0
- package/docs/admin-dns-certificates.md +122 -0
- package/docs/admin-dns-records.md +65 -0
- package/docs/admin-dns-registrar.md +33 -0
- package/docs/admin-dns.md +80 -0
- package/docs/admin-identity-users.md +103 -0
- package/docs/admin-incidents.md +71 -0
- package/docs/admin-jobs.md +426 -0
- package/docs/admin-members.md +112 -0
- package/docs/admin-messaging.md +56 -0
- package/docs/admin-metrics-explorer.md +63 -0
- package/docs/admin-monitoring.md +99 -0
- package/docs/admin-network-security.md +393 -0
- package/docs/admin-phonehub.md +27 -0
- package/docs/admin-push.md +36 -0
- package/docs/admin-rules.md +87 -0
- package/docs/admin-security-tickets.md +40 -0
- package/docs/admin-settings.md +78 -0
- package/docs/admin-shortlinks.md +32 -0
- package/docs/admin-storage.md +141 -0
- package/docs/auth-pages.md +212 -0
- package/docs/auth.md +58 -0
- package/docs/calendar.md +122 -0
- package/docs/charts.md +303 -0
- package/docs/client.md +206 -0
- package/docs/collection-multiselect.md +117 -0
- package/docs/collection-select.md +132 -0
- package/docs/combobox.md +105 -0
- package/docs/dataview.md +226 -0
- package/docs/datepicker.md +151 -0
- package/docs/daterange.md +170 -0
- package/docs/datetimepicker.md +249 -0
- package/docs/defineModel.md +72 -0
- package/docs/detail-primitives.md +174 -0
- package/docs/feedback.md +85 -0
- package/docs/file-upload-ux.md +146 -0
- package/docs/forms.md +435 -0
- package/docs/grouping-and-fmt.md +98 -0
- package/docs/idioms.md +112 -0
- package/docs/image-editor.md +101 -0
- package/docs/loading.md +242 -0
- package/docs/location-address.md +76 -0
- package/docs/markdown.md +203 -0
- package/docs/menus-and-access.md +89 -0
- package/docs/multiselect-dropdown.md +153 -0
- package/docs/params.md +73 -0
- package/docs/popover.md +130 -0
- package/docs/realtime.md +61 -0
- package/docs/record-feed.md +238 -0
- package/docs/right-panel.md +42 -0
- package/docs/tabs-and-form-wizard.md +60 -0
- package/docs/taginput.md +120 -0
- package/docs/timepicker.md +173 -0
- package/docs/timezone-select.md +147 -0
- package/docs/worldmap.md +302 -0
- package/package.json +55 -0
- package/src/admin/assistant/AssistantFeed.tsx +29 -0
- package/src/admin/assistant/AssistantPanel.tsx +65 -0
- package/src/admin/assistant/api.ts +17 -0
- package/src/admin/assistant/data.ts +63 -0
- package/src/admin/assistant/index.ts +21 -0
- package/src/admin/assistant/launchers.tsx +33 -0
- package/src/admin/assistant/pages.tsx +78 -0
- package/src/admin/assistant/streaming.ts +238 -0
- package/src/admin/assistant/types.ts +30 -0
- package/src/admin/bouncer/devices.tsx +230 -0
- package/src/admin/bouncer/index.ts +34 -0
- package/src/admin/bouncer/models.ts +199 -0
- package/src/admin/bouncer/signals.tsx +182 -0
- package/src/admin/bouncer/signatures.tsx +220 -0
- package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
- package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
- package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
- package/src/admin/cloudwatch/data.ts +163 -0
- package/src/admin/cloudwatch/index.ts +16 -0
- package/src/admin/credentials/group-api-keys.tsx +433 -0
- package/src/admin/credentials/index.ts +4 -0
- package/src/admin/credentials/models.ts +244 -0
- package/src/admin/credentials/secret-dialog.tsx +77 -0
- package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
- package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
- package/src/admin/dashboard/data.ts +60 -0
- package/src/admin/dashboard/index.ts +2 -0
- package/src/admin/dns/CertificateDetail.tsx +115 -0
- package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
- package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
- package/src/admin/dns/CertificatesPage.tsx +85 -0
- package/src/admin/dns/DnsRecordEditor.tsx +110 -0
- package/src/admin/dns/DnsRecordsPage.tsx +36 -0
- package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
- package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
- package/src/admin/dns/DomainDetail.tsx +41 -0
- package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
- package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
- package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
- package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
- package/src/admin/dns/DomainsPage.tsx +27 -0
- package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
- package/src/admin/dns/RegistrantContactPage.tsx +42 -0
- package/src/admin/dns/api.ts +515 -0
- package/src/admin/dns/certificate-data.ts +221 -0
- package/src/admin/dns/data.ts +32 -0
- package/src/admin/dns/dns-data.ts +266 -0
- package/src/admin/dns/dns-integration.ts +24 -0
- package/src/admin/dns/domain-links.ts +37 -0
- package/src/admin/dns/index.ts +86 -0
- package/src/admin/dns/models.ts +418 -0
- package/src/admin/dns/purchase-data.ts +67 -0
- package/src/admin/dns/registrant-data.ts +52 -0
- package/src/admin/identity/index.ts +5 -0
- package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
- package/src/admin/identity/members/MemberDetail.tsx +226 -0
- package/src/admin/identity/members/MembersPage.tsx +144 -0
- package/src/admin/identity/members/index.ts +24 -0
- package/src/admin/identity/members/member-flows.tsx +132 -0
- package/src/admin/identity/members/models.ts +204 -0
- package/src/admin/identity/users/UserDetail.tsx +257 -0
- package/src/admin/identity/users/UsersPage.tsx +163 -0
- package/src/admin/identity/users/index.ts +27 -0
- package/src/admin/identity/users/models.ts +232 -0
- package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
- package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
- package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
- package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
- package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
- package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
- package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
- package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
- package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
- package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
- package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
- package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
- package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
- package/src/admin/identity/users/sections/actions.tsx +752 -0
- package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
- package/src/admin/identity/users/sections/queries.ts +74 -0
- package/src/admin/identity/users/sections/shared.tsx +299 -0
- package/src/admin/incidents/EventDetail.tsx +43 -0
- package/src/admin/incidents/EventsPage.tsx +52 -0
- package/src/admin/incidents/IncidentDetail.tsx +68 -0
- package/src/admin/incidents/IncidentsPage.tsx +102 -0
- package/src/admin/incidents/forensics.tsx +73 -0
- package/src/admin/incidents/index.ts +16 -0
- package/src/admin/incidents/models.ts +113 -0
- package/src/admin/incidents/sanitize.ts +71 -0
- package/src/admin/index.ts +383 -0
- package/src/admin/jobs/JobDashboardPage.tsx +111 -0
- package/src/admin/jobs/JobDetail.tsx +721 -0
- package/src/admin/jobs/JobRunnersPage.tsx +216 -0
- package/src/admin/jobs/JobsTablePage.tsx +122 -0
- package/src/admin/jobs/RunnerDetail.tsx +576 -0
- package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
- package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
- package/src/admin/jobs/columns.tsx +297 -0
- package/src/admin/jobs/control.ts +488 -0
- package/src/admin/jobs/index.ts +85 -0
- package/src/admin/jobs/models.ts +599 -0
- package/src/admin/jobs/queries.ts +272 -0
- package/src/admin/jobs/scheduled-task-form.tsx +493 -0
- package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
- package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
- package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
- package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
- package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
- package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
- package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
- package/src/admin/messaging/MailboxesPage.tsx +15 -0
- package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
- package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
- package/src/admin/messaging/SentMessagesPage.tsx +11 -0
- package/src/admin/messaging/api.ts +17 -0
- package/src/admin/messaging/data.ts +10 -0
- package/src/admin/messaging/index.ts +23 -0
- package/src/admin/messaging/models.ts +76 -0
- package/src/admin/messaging/push/PushPage.tsx +44 -0
- package/src/admin/messaging/push/api.ts +17 -0
- package/src/admin/messaging/push/index.ts +9 -0
- package/src/admin/messaging/push/models.ts +43 -0
- package/src/admin/messaging/sanitize.ts +44 -0
- package/src/admin/monitoring/LogInspector.tsx +160 -0
- package/src/admin/monitoring/LogsPage.tsx +135 -0
- package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
- package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
- package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
- package/src/admin/monitoring/index.ts +55 -0
- package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
- package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
- package/src/admin/monitoring/models.ts +155 -0
- package/src/admin/network/BlockedIPsPage.tsx +194 -0
- package/src/admin/network/BlockedIpDetail.tsx +262 -0
- package/src/admin/network/FirewallLogPage.tsx +115 -0
- package/src/admin/network/IPSetDetail.tsx +280 -0
- package/src/admin/network/IPSetEditor.tsx +191 -0
- package/src/admin/network/IPSetsPage.tsx +177 -0
- package/src/admin/network/geofence/BlocksTab.tsx +289 -0
- package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
- package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
- package/src/admin/network/geofence/PostureHeader.tsx +184 -0
- package/src/admin/network/geofence/RuleEditor.tsx +176 -0
- package/src/admin/network/geofence/RulesTab.tsx +254 -0
- package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
- package/src/admin/network/geofence/geofence-data.ts +674 -0
- package/src/admin/network/index.ts +68 -0
- package/src/admin/network/models.ts +607 -0
- package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
- package/src/admin/phonehub/api.ts +47 -0
- package/src/admin/phonehub/data.ts +6 -0
- package/src/admin/phonehub/index.ts +10 -0
- package/src/admin/phonehub/models.ts +68 -0
- package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
- package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
- package/src/admin/rules/RuleSetsPage.tsx +33 -0
- package/src/admin/rules/editors.tsx +54 -0
- package/src/admin/rules/handler-dsl.ts +213 -0
- package/src/admin/rules/index.ts +13 -0
- package/src/admin/rules/models.ts +112 -0
- package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
- package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
- package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
- package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
- package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
- package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
- package/src/admin/security/devices/index.ts +40 -0
- package/src/admin/security/devices/models.ts +470 -0
- package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
- package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
- package/src/admin/security/geoip/geoip-forms.ts +102 -0
- package/src/admin/security/geoip/index.ts +27 -0
- package/src/admin/security/geoip/models.ts +397 -0
- package/src/admin/security/index.ts +31 -0
- package/src/admin/security/models.ts +137 -0
- package/src/admin/security/tickets.tsx +562 -0
- package/src/admin/security-permissions.ts +5 -0
- package/src/admin/settings/SettingDetail.tsx +57 -0
- package/src/admin/settings/SettingEditor.tsx +157 -0
- package/src/admin/settings/SettingsPage.tsx +72 -0
- package/src/admin/settings/index.ts +19 -0
- package/src/admin/settings/model.ts +143 -0
- package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
- package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
- package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
- package/src/admin/shortlinks/index.ts +18 -0
- package/src/admin/shortlinks/models.ts +246 -0
- package/src/admin/storage/BackendsPage.tsx +43 -0
- package/src/admin/storage/BucketDetail.tsx +77 -0
- package/src/admin/storage/BucketsPage.tsx +64 -0
- package/src/admin/storage/FileManagerDetail.tsx +36 -0
- package/src/admin/storage/FilePreview.tsx +54 -0
- package/src/admin/storage/FileUploadSurface.tsx +250 -0
- package/src/admin/storage/FileView.tsx +109 -0
- package/src/admin/storage/FilesPage.tsx +53 -0
- package/src/admin/storage/file-renditions.ts +99 -0
- package/src/admin/storage/index.ts +31 -0
- package/src/admin/storage/models.ts +585 -0
- package/src/admin/storage/storage-dialogs.tsx +80 -0
- package/src/charts/CircularProgress.tsx +281 -0
- package/src/charts/KPIStrip.tsx +162 -0
- package/src/charts/KPITile.tsx +125 -0
- package/src/charts/MetricsChart.tsx +459 -0
- package/src/charts/MetricsMiniWidget.tsx +588 -0
- package/src/charts/MiniChart.tsx +283 -0
- package/src/charts/PieChart.tsx +354 -0
- package/src/charts/SeriesChart.tsx +378 -0
- package/src/charts/chart-dialogs.tsx +149 -0
- package/src/charts/exportChart.ts +144 -0
- package/src/charts/index.ts +28 -0
- package/src/charts/pie-math.ts +127 -0
- package/src/charts/stats.ts +265 -0
- package/src/charts/worldmap/WorldMap.tsx +649 -0
- package/src/charts/worldmap/countryCentroids.ts +304 -0
- package/src/charts/worldmap/geo.ts +293 -0
- package/src/charts/worldmap/use-world-land.ts +47 -0
- package/src/charts/worldmap/world-land.ts +18 -0
- package/src/charts/worldmap/worldmap-data.ts +285 -0
- package/src/client/auth.ts +737 -0
- package/src/client/client.ts +268 -0
- package/src/client/duid.ts +31 -0
- package/src/client/errors.ts +33 -0
- package/src/client/group-context.ts +28 -0
- package/src/client/group.tsx +126 -0
- package/src/client/hooks.ts +50 -0
- package/src/client/index.ts +37 -0
- package/src/client/jwt.ts +75 -0
- package/src/client/location.ts +285 -0
- package/src/client/lookups.ts +41 -0
- package/src/client/markdown-fallback.tsx +115 -0
- package/src/client/markdown-parse.ts +386 -0
- package/src/client/markdown-sanitize.ts +217 -0
- package/src/client/markdown.ts +122 -0
- package/src/client/me.ts +188 -0
- package/src/client/mock.ts +9984 -0
- package/src/client/model.ts +227 -0
- package/src/client/params.ts +292 -0
- package/src/client/realtime-mock.ts +221 -0
- package/src/client/realtime.ts +561 -0
- package/src/client/record-feed.ts +329 -0
- package/src/client/safe-export.ts +71 -0
- package/src/client/types.ts +278 -0
- package/src/client/upload.ts +421 -0
- package/src/ui/AddressField.tsx +273 -0
- package/src/ui/AttachmentQueue.tsx +92 -0
- package/src/ui/CollectionMultiSelect.tsx +622 -0
- package/src/ui/CollectionSelect.tsx +517 -0
- package/src/ui/ComboBox.tsx +336 -0
- package/src/ui/DetailView.tsx +438 -0
- package/src/ui/FileDrop.tsx +194 -0
- package/src/ui/FileField.tsx +369 -0
- package/src/ui/FilterBar.tsx +269 -0
- package/src/ui/FormFields.tsx +77 -0
- package/src/ui/FormView.tsx +360 -0
- package/src/ui/FormWizard.tsx +336 -0
- package/src/ui/GroupSwitcher.tsx +202 -0
- package/src/ui/Guarded.tsx +16 -0
- package/src/ui/MarkdownView.tsx +89 -0
- package/src/ui/ModelTable.tsx +848 -0
- package/src/ui/MultiSelectDropdown.tsx +390 -0
- package/src/ui/Popover.tsx +204 -0
- package/src/ui/RecordFeed.tsx +627 -0
- package/src/ui/RequiresGroup.tsx +32 -0
- package/src/ui/RightPanel.tsx +107 -0
- package/src/ui/SidebarNav.tsx +330 -0
- package/src/ui/Tabs.tsx +161 -0
- package/src/ui/TagInput.tsx +381 -0
- package/src/ui/ThemeProvider.tsx +52 -0
- package/src/ui/UploadQueue.tsx +603 -0
- package/src/ui/armed-button.tsx +91 -0
- package/src/ui/date/Calendar.tsx +498 -0
- package/src/ui/date/DatePicker.tsx +289 -0
- package/src/ui/date/DateRangePicker.tsx +280 -0
- package/src/ui/date/DateTimePicker.tsx +587 -0
- package/src/ui/date/PresetRail.tsx +165 -0
- package/src/ui/date/TimePicker.tsx +624 -0
- package/src/ui/date/TimezoneSelect.tsx +267 -0
- package/src/ui/date/fns.ts +679 -0
- package/src/ui/detail/DataView.tsx +692 -0
- package/src/ui/detail/FlowStrip.tsx +80 -0
- package/src/ui/detail/KnownFieldsCard.tsx +121 -0
- package/src/ui/detail/MetadataSection.tsx +219 -0
- package/src/ui/detail/StackTraceView.tsx +150 -0
- package/src/ui/detail/StatusPanel.tsx +73 -0
- package/src/ui/detail/Timeline.tsx +52 -0
- package/src/ui/detail/index.ts +12 -0
- package/src/ui/field-registry.tsx +428 -0
- package/src/ui/field-wire.ts +322 -0
- package/src/ui/form-autosave.ts +583 -0
- package/src/ui/format.ts +444 -0
- package/src/ui/grouping.ts +150 -0
- package/src/ui/image-editor/ImageEditor.tsx +484 -0
- package/src/ui/image-editor/canvas.ts +254 -0
- package/src/ui/image-editor/index.ts +3 -0
- package/src/ui/image-editor/math.ts +416 -0
- package/src/ui/index.ts +58 -0
- package/src/ui/loading.tsx +604 -0
- package/src/ui/menu-registry.ts +213 -0
- package/src/ui/modal.tsx +297 -0
- package/src/ui/password.tsx +302 -0
- package/src/ui/schema-form-core.tsx +315 -0
- package/src/ui/toast.tsx +219 -0
- package/src/ui/ui.tsx +45 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# Admin: network security — blocks, IP sets, geofencing
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import {
|
|
5
|
+
// the section (registered in ADMIN_SECTIONS — no app wiring needed)
|
|
6
|
+
NETWORK_SECURITY_ADMIN_SECTION,
|
|
7
|
+
// pages
|
|
8
|
+
BlockedIPsPage, FirewallLogPage, IPSetsPage, GeofencingPage,
|
|
9
|
+
// KISS detail modals (#1425)
|
|
10
|
+
showBlockedIpDetail, showIPSetDetail,
|
|
11
|
+
// the SHARED rule editor — platform page and group panel render this one
|
|
12
|
+
GeofenceRuleEditor, makeRuleEditorValue, ruleFromEditorValue, toggleRuleEditorMode,
|
|
13
|
+
// models
|
|
14
|
+
IPSetModel, GeofenceEventModel,
|
|
15
|
+
// permission clauses
|
|
16
|
+
IPSET_VIEW_PERMS, IPSET_MANAGE_PERMS, IPSET_DELETE_PERMS,
|
|
17
|
+
GEOFENCE_VIEW_PERMS, GEOFENCE_MANAGE_PERMS, SECURITY_EVENTS_PERMS,
|
|
18
|
+
METRICS_GLOBAL_VIEW_PERMS, GROUP_GEOFENCE_EDIT_PERMS,
|
|
19
|
+
// the geofence projection (one module, two scopes)
|
|
20
|
+
isAdvancedRule, ruleToForm, formToRule, describeRule, diffRules,
|
|
21
|
+
describeDecision, describeWouldBlock, buildSimulateBody, buildGroupRulePayload,
|
|
22
|
+
coerceRuleInput, collectScopes, scopeLabel, regionName,
|
|
23
|
+
ABUSE_FLAGS, US_STATES, COUNTRY_OPTIONS, countryName,
|
|
24
|
+
// config-plane hooks (all `enabled`-gated — a denied surface issues NO request)
|
|
25
|
+
useGeoConfig, useGeoAllowlist, useBypassHolders, useGeoSimulate,
|
|
26
|
+
useSaveGeoRules, useRemoveGeoRulesOverride, useSaveGeoAllowlist,
|
|
27
|
+
useGeofenceCountryTotals, useIPSetCidrData,
|
|
28
|
+
isGeofenceApiMissing, GEOFENCE_API_MISSING_MESSAGE,
|
|
29
|
+
} from 'portal-mojo/admin';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Perimeter control: block an abusive IP right now, whitelist a customer's NAT,
|
|
33
|
+
maintain country and datacenter CIDR sets synced to the firewall fleet, set
|
|
34
|
+
geographic policy — and prove the policy does what you think with a built-in
|
|
35
|
+
simulator before it bites real traffic. Demo: showcase → Admin → **Network
|
|
36
|
+
security**. Styles: `apps/{portal,showcase}/src/theme/admin-network.css`
|
|
37
|
+
(byte-identical).
|
|
38
|
+
|
|
39
|
+
## Surfaces
|
|
40
|
+
|
|
41
|
+
One section, `network-security` at `security/network`, under the **Security**
|
|
42
|
+
navigation group beside Security Operations, Bouncer, Devices & Logins and IP
|
|
43
|
+
Intelligence.
|
|
44
|
+
|
|
45
|
+
| Route | Presentation | Gate |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `blocked-ips` | page + `modal.detail` | `GEOIP_VIEW_PERMS` |
|
|
48
|
+
| `firewall-log` | page + the shipped `LogInspector` | `FIREWALL_LOG_PERMS` |
|
|
49
|
+
| `ip-sets` | page + a two-tab `modal.detail` | `IPSET_VIEW_PERMS` |
|
|
50
|
+
| `geofencing` | **page**: posture header over 4 tabs | `GEOFENCE_VIEW_PERMS` |
|
|
51
|
+
|
|
52
|
+
Geofencing is the one surface that stays a page. That is the explicitly
|
|
53
|
+
requested presentation: a config page that states what is in force and lets you
|
|
54
|
+
test it before it bites.
|
|
55
|
+
|
|
56
|
+
The section gate is the ANY-of **union** of the four route gates (the
|
|
57
|
+
`MONITORING_ADMIN_SECTION` pattern), and each route then carries its exact
|
|
58
|
+
backend gate. A geofence-only operator (`sys.view_geofence` alone) reaches
|
|
59
|
+
Geofencing and is denied everywhere else; the engine's landing redirect resolves
|
|
60
|
+
to the first route they can see, so nobody lands on a denial.
|
|
61
|
+
|
|
62
|
+
## Permissions — `sys.`-pinned global grants only, fail-closed
|
|
63
|
+
|
|
64
|
+
`@md.requires_global_perms` authorizes against global `User.permissions` or
|
|
65
|
+
superuser **only** — no group fallback, and a `GroupScopedToken` is refused
|
|
66
|
+
outright. Every clause here is therefore `sys.`-pinned: a member grant must
|
|
67
|
+
never open platform enforcement config.
|
|
68
|
+
|
|
69
|
+
| Constant | Value | Backend source |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `GEOIP_VIEW_PERMS` | (imported from #1291) | `GeoLocatedIP.RestMeta.VIEW_PERMS` |
|
|
72
|
+
| `GEOIP_MANAGE_PERMS` | (imported from #1291) | `GeoLocatedIP.RestMeta.SAVE_PERMS` |
|
|
73
|
+
| `IPSET_VIEW_PERMS` | `sys.view_security`, `sys.security` | `IPSet.RestMeta.VIEW_PERMS` |
|
|
74
|
+
| `IPSET_MANAGE_PERMS` | `sys.manage_security`, `sys.security` | `IPSet.RestMeta.SAVE_PERMS` |
|
|
75
|
+
| `IPSET_DELETE_PERMS` | `sys.manage_security` **only** | `IPSet.RestMeta.DELETE_PERMS` |
|
|
76
|
+
| `FIREWALL_LOG_PERMS` | `LOGS_ADMIN_PERMISSIONS` (reused) | `logit.Log.RestMeta.VIEW_PERMS` |
|
|
77
|
+
| `GEOFENCE_VIEW_PERMS` | `sys.view_geofence`, `sys.manage_geofence`, `sys.security` | `@requires_global_perms` on `GET /api/geo/rules` |
|
|
78
|
+
| `GEOFENCE_MANAGE_PERMS` | `sys.manage_geofence`, `sys.security` | `POST`/`DELETE /api/geo/rules` |
|
|
79
|
+
| `SECURITY_EVENTS_PERMS` | `SECURITY_VIEW_PERMS` (reused) | `incident.Event.RestMeta.VIEW_PERMS` |
|
|
80
|
+
| `METRICS_GLOBAL_VIEW_PERMS` | `sys.view_metrics`, `sys.metrics` | `metrics/rest/helpers.check_view_permissions` for `account="global"` |
|
|
81
|
+
|
|
82
|
+
Note the two traps the table encodes:
|
|
83
|
+
|
|
84
|
+
- **`IPSET_DELETE_PERMS` is narrower than manage.** The broad `security` grant
|
|
85
|
+
saves an IP set but cannot delete one.
|
|
86
|
+
- **`manage_metrics` does NOT imply `view_metrics`.** An operator holding only
|
|
87
|
+
the manage grant is denied `account="global"` reads, so the blocks tab's
|
|
88
|
+
metrics strip gates on the view clause rather than firing and catching a 403.
|
|
89
|
+
|
|
90
|
+
A permission-disabled query passes `enabled: false`. **No denied surface in this
|
|
91
|
+
module issues a request** — not the metrics strip, not the blocks table, not the
|
|
92
|
+
change history, not the posture header's Last-change chip.
|
|
93
|
+
|
|
94
|
+
## Endpoints
|
|
95
|
+
|
|
96
|
+
| Endpoint | Gate | Notes |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `GET/POST /api/incident/ipset` `(+/<pk>)` | model `VIEW`/`SAVE`/`DELETE_PERMS` | graphs `default` · `detailed`; actions `sync`, `enable`, `disable`, `refresh_source` |
|
|
99
|
+
| `GET /api/system/geoip` `(+/<pk>)` | #1291's model | Blocked IPs is the `is_blocked=true` projection |
|
|
100
|
+
| `GET /api/logs` | `logit.Log` | filtered `kind__startswith=firewall:` |
|
|
101
|
+
| `GET/POST/DELETE /api/geo/rules` | global geofence grants | `POST` body is `{rule}`, a FULL REPLACE |
|
|
102
|
+
| `POST /api/geo/simulate` | global geofence view | body keys `ip`, `geo`, `scope`, `group_uuid` |
|
|
103
|
+
| `GET/POST /api/geo/allowlist` | view / manage | `POST` body is `{entries}`, a FULL REPLACE |
|
|
104
|
+
| `GET /api/geo/bypass_holders` | global geofence view | read-only, capped at 200 |
|
|
105
|
+
| `GET /api/incident/event` | `SECURITY_EVENTS_PERMS` | `category=geofence_block\|geofence_exempt\|geofence_config` |
|
|
106
|
+
| `GET /api/metrics/category_slugs` | `check_view_permissions` | **flat envelope — no `data` wrapper** |
|
|
107
|
+
| `GET /api/metrics/fetch` | same | `account="global"` |
|
|
108
|
+
|
|
109
|
+
### `GET /api/geo/rules` — the config payload, key for key
|
|
110
|
+
|
|
111
|
+
```jsonc
|
|
112
|
+
{
|
|
113
|
+
"system": { "rule": {…}, "source": "setting|conf|none", "modified": "ISO|null" },
|
|
114
|
+
"group": { "id", "uuid", "is_active", "rule", "strict_posture", "strict_posture_effective" },
|
|
115
|
+
"posture": {
|
|
116
|
+
"enabled", "fail_closed", "fail_closed_scopes",
|
|
117
|
+
"allow_private_ips", "strict_posture", "cache_ttl"
|
|
118
|
+
},
|
|
119
|
+
"allowlist_summary": { "setting_entries", "geoip_active" },
|
|
120
|
+
"evaluation_order": ["system", "group"],
|
|
121
|
+
"enforced_endpoints": [{ "endpoint", "scope", "after_auth?" }]
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`group` is present **only** when the request carried `group_uuid`; an unknown
|
|
126
|
+
uuid is a 400, and an *inactive* group is deliberately returned and evaluated.
|
|
127
|
+
`after_auth` appears only when truthy.
|
|
128
|
+
|
|
129
|
+
### The decision shape
|
|
130
|
+
|
|
131
|
+
`_build_decision` writes `allowed`, `reason`, `detail`, `ip`, `country`,
|
|
132
|
+
`country_code`, `region`, `region_code`, `abuse`, `checked_at`, `rule_level`,
|
|
133
|
+
`strict_posture`. `country` and `country_code` carry the **same** value, as do
|
|
134
|
+
`region` and `region_code`.
|
|
135
|
+
|
|
136
|
+
**`enabled` sits at the TOP level of a simulate decision** (`engine.py:473`) and
|
|
137
|
+
is absent from `check()` decisions entirely. `would_block`, `would_block_reason`,
|
|
138
|
+
`allowlist_source`, `allowlist_reason` and `allowlist_until` appear **only** when
|
|
139
|
+
the IP matched the allowlist — and `would_block` is `null` (not `false`) when the
|
|
140
|
+
shadow decision was itself a `lookup_failed`, because the engine genuinely does
|
|
141
|
+
not know. `_allowlisted_decision` never copies `rule_level`, so an exempt
|
|
142
|
+
decision's rule level is always `null`.
|
|
143
|
+
|
|
144
|
+
### `/api/metrics/category_slugs` — the flat-envelope exception
|
|
145
|
+
|
|
146
|
+
This route returns a raw `JsonResponse`, so the payload is **not** nested under
|
|
147
|
+
`data`:
|
|
148
|
+
|
|
149
|
+
```jsonc
|
|
150
|
+
{ "slugs": [...], "category": "geofence", "account": "global", "status": true }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The `account` default is `"public"` while `metrics.record()` defaults to
|
|
154
|
+
`"global"` — so a geofence slug is invisible unless `account=global` is asked
|
|
155
|
+
for explicitly. `get_category_slugs` returns a **set**, so slug order is
|
|
156
|
+
nondeterministic and no consumer may depend on it.
|
|
157
|
+
|
|
158
|
+
## The rule DSL and the guided↔advanced projection
|
|
159
|
+
|
|
160
|
+
Top-level keys `{country, region, abuse}`; operators `{in, not_in, eq}`; abuse
|
|
161
|
+
flags `{tor, vpn, datacenter, proxy}`. Abuse semantics: `false` blocks when the
|
|
162
|
+
flag is detected, `true` REQUIRES the flag (rare), absent/`null` means don't
|
|
163
|
+
care.
|
|
164
|
+
|
|
165
|
+
`geofence-data.ts` is the ONE definition of the lossy projection between that
|
|
166
|
+
DSL and the friendly form. `apps/portal/src/pages/group-sections/geofence-data.ts`
|
|
167
|
+
is a re-export shim over it, so the platform page and the group panel cannot
|
|
168
|
+
drift. `GeofenceRuleEditor` is the ONE editor both render.
|
|
169
|
+
|
|
170
|
+
The projection's contract:
|
|
171
|
+
|
|
172
|
+
- `isAdvancedRule(rule)` — true for anything the guided form cannot represent:
|
|
173
|
+
unknown top keys, non-dict bodies, `country.eq`, multiple operators, an
|
|
174
|
+
allow+block mix, any `region` shape other than a single `not_in` of US state
|
|
175
|
+
codes, and any abuse `true` or unknown flag.
|
|
176
|
+
- An advanced-forced rule **opens in JSON with no toggle** — flipping to guided
|
|
177
|
+
would silently drop clauses.
|
|
178
|
+
- The toggle back to guided **refuses** invalid JSON and unrepresentable shapes;
|
|
179
|
+
the guided→JSON direction carries unsaved edits across.
|
|
180
|
+
- `buildGroupRulePayload(old, new)` — GROUP layer only. django deep-merges
|
|
181
|
+
JSONFields (`null` deletes a key; nested `__replace` is NOT supported), so the
|
|
182
|
+
payload is the new value for every kept key plus explicit `null`s for anything
|
|
183
|
+
removed. Sub-keys are **allowlisted**, so a `__replace` typed into the advanced
|
|
184
|
+
editor is dropped rather than PATCHed into stored metadata.
|
|
185
|
+
- The platform layer never merges: `POST /api/geo/rules` is a full replace, and
|
|
186
|
+
the two surfaces share no save path. **The group surface can only tighten and
|
|
187
|
+
can never write platform rules; the global surface never writes group
|
|
188
|
+
metadata.**
|
|
189
|
+
|
|
190
|
+
Every reason code in `engine._DETAIL_MAP` has plain-language copy, including
|
|
191
|
+
**`no_rules_strict`** — the strict-posture denial web-mojo had no text for.
|
|
192
|
+
An unknown code degrades to a readable fallback **and warns once**.
|
|
193
|
+
|
|
194
|
+
## Event metadata keys
|
|
195
|
+
|
|
196
|
+
`services/geofence/evidence.py` writes, for `geofence_block`: `reason`,
|
|
197
|
+
`rule_level`, **`geofence_scope`**, `country_code`, `region_code`, `abuse`,
|
|
198
|
+
`detail`, and `username` when the request was authenticated. For
|
|
199
|
+
`geofence_exempt`: `reason`, `geofence_scope`, `allowlist_source`,
|
|
200
|
+
`allowlist_reason`, `would_block_reason`, `country_code`, `region_code`. For
|
|
201
|
+
`geofence_config`: `target`, `old`, `new`, **`changed_by`**, `changed_by_id`.
|
|
202
|
+
|
|
203
|
+
Two keys this module reads that web-mojo got wrong:
|
|
204
|
+
|
|
205
|
+
- It is **`metadata.geofence_scope`**, never `metadata.scope`. The reporter
|
|
206
|
+
never passes the reporter's `scope=` argument, so the top-level `Event.scope`
|
|
207
|
+
column stays `"global"` for every geofence row.
|
|
208
|
+
- The config "who" is **`metadata.changed_by`** (a username), falling back to
|
|
209
|
+
`metadata.user_name` (the display name, written by
|
|
210
|
+
`reporter._create_event_dict`). `metadata.username` is written by neither.
|
|
211
|
+
|
|
212
|
+
`Event.country_code` is a real indexed column — `Event.sync_metadata` fills it
|
|
213
|
+
from the geolocated source IP — so country filtering uses the column, not the
|
|
214
|
+
metadata copy.
|
|
215
|
+
|
|
216
|
+
`_block_level`: **7** rule_invalid (a broken rule is denying traffic; the default
|
|
217
|
+
incident threshold auto-creates an Incident) · **6** lookup failure while failing
|
|
218
|
+
open (enforcement silently is not happening) · **5** abuse-flag block, a block on
|
|
219
|
+
a fail-closed scope, or any block under strict posture · **3** ordinary
|
|
220
|
+
jurisdiction block, exemption used, or config change.
|
|
221
|
+
|
|
222
|
+
## IP sets
|
|
223
|
+
|
|
224
|
+
`KIND_CHOICES` are `country · datacenter · abuse · custom`. `SOURCE_CHOICES` has
|
|
225
|
+
**five** members — `ipdeny`, `abuseipdb`, `tor`, `blocklist_de`, `manual`.
|
|
226
|
+
web-mojo listed three, so the two cache-only rows showed a raw slug and could not
|
|
227
|
+
be filtered.
|
|
228
|
+
|
|
229
|
+
### Records are created disabled, and `is_enabled` is never a plain field
|
|
230
|
+
|
|
231
|
+
`on_action_enable` is the **only** path that runs the cache-only rejection.
|
|
232
|
+
Writing `is_enabled` as a plain field produces a row that reads "Enabled" and
|
|
233
|
+
silently never syncs, because `sync()` no-ops for a cache-only set without
|
|
234
|
+
raising. So:
|
|
235
|
+
|
|
236
|
+
- the create form has no enable switch and the POST body carries no
|
|
237
|
+
`is_enabled` — a set is created **staged**;
|
|
238
|
+
- the edit form has no `is_enabled` either;
|
|
239
|
+
- enabling and disabling are armed **actions**.
|
|
240
|
+
|
|
241
|
+
Direct #1097 lineage; the verifier asserts it against source.
|
|
242
|
+
|
|
243
|
+
### `tor_exits` and `blocklist_de` are cache-only
|
|
244
|
+
|
|
245
|
+
`IPSet.THREAT_CACHE_SETS`. They exist to feed GeoIP threat *detection*, not the
|
|
246
|
+
kernel firewall. `enable` answers a 400 that names the set:
|
|
247
|
+
|
|
248
|
+
> `'tor_exits' is a cache-only threat list for geoip detection — enabling it
|
|
249
|
+
> would kernel-block every listed IP fleet-wide and is not permitted`
|
|
250
|
+
|
|
251
|
+
The UI surfaces that message verbatim, chips the rows **CACHE-ONLY**, and
|
|
252
|
+
excludes them from the batch Enable and Sync eligibility so a mixed selection
|
|
253
|
+
still succeeds for the rest.
|
|
254
|
+
|
|
255
|
+
### `data` must be posted as a LIST
|
|
256
|
+
|
|
257
|
+
`IPSet` defines `set_data(cidr_list)` = `"\n".join(cidr_list)`, and
|
|
258
|
+
`on_rest_save_field` prefers a `set_<key>` method over a plain assignment. So a
|
|
259
|
+
posted `data` runs through the setter:
|
|
260
|
+
|
|
261
|
+
- a **JSON list** is correct, and `cidr_count` is recomputed from it;
|
|
262
|
+
- a **string** gets a newline interleaved between every *character* and
|
|
263
|
+
`cidr_count` becomes the character count.
|
|
264
|
+
|
|
265
|
+
web-mojo posted the raw textarea string. `parseCidrLines()` is the boundary here;
|
|
266
|
+
the mock reproduces both behaviours so the trap stays executable.
|
|
267
|
+
|
|
268
|
+
This also corrects a claim recorded during scoping: `cidr_count` is **not**
|
|
269
|
+
frozen at 0 for manual sets forever. It is stale only until the list is next
|
|
270
|
+
*written* — by a source refresh, or by saving the list from the editor.
|
|
271
|
+
`refresh_from_source()` really does return `False` immediately for
|
|
272
|
+
`source == 'manual'`, so the *refresh* path alone can never update it. The CIDR
|
|
273
|
+
Data tab shows the parsed line count beside the stored one whenever they differ.
|
|
274
|
+
|
|
275
|
+
## Firewall log — column honesty
|
|
276
|
+
|
|
277
|
+
`GeoLocatedIP` writes these rows through `MojoModel.log`, which fills `path`,
|
|
278
|
+
`ip`, `username` and `uid` from the **ambient request** — the admin's endpoint
|
|
279
|
+
and the admin's address. The blocked address lives only in `payload.ip`.
|
|
280
|
+
web-mojo labelled `path` as "IP / Path", which was never either.
|
|
281
|
+
|
|
282
|
+
The four `kind` values are exact, and the payload shape is **not** uniform:
|
|
283
|
+
|
|
284
|
+
| kind | payload keys |
|
|
285
|
+
|---|---|
|
|
286
|
+
| `firewall:block` | `ip, reason, ttl, blocked_until, block_count, trigger` |
|
|
287
|
+
| `firewall:unblock` | `ip, reason, trigger` |
|
|
288
|
+
| `firewall:whitelist` | `ip, reason, until, was_blocked, trigger` |
|
|
289
|
+
| `firewall:unwhitelist` | `ip, trigger` |
|
|
290
|
+
|
|
291
|
+
## Blocked IPs
|
|
292
|
+
|
|
293
|
+
The `is_blocked=true` projection over #1291's `GeoLocatedIPModel`. This module
|
|
294
|
+
defines **no** model for `/api/system/geoip` — one endpoint, one `defineModel`,
|
|
295
|
+
one cache key.
|
|
296
|
+
|
|
297
|
+
- **Enforcement is computed.** `block_active` and `whitelist_active` are Python
|
|
298
|
+
properties serialized only on the `basic` graph, so `blockActive(row)` /
|
|
299
|
+
`whitelistActive(row)` (#1291's helpers) decide the State column: a whitelist
|
|
300
|
+
beats a block, an expired block stops enforcing, and `is_blocked` stays set
|
|
301
|
+
either way.
|
|
302
|
+
- **`blocked_until = null` means PERMANENT** and renders "Never", not blank.
|
|
303
|
+
- The threat filter is a multiselect over the four real values plus an explicit
|
|
304
|
+
`threat_level__isnull` filter. web-mojo offered a literal `none` value that
|
|
305
|
+
matched no row on any deployment.
|
|
306
|
+
- The search placeholder names the model's real `SEARCH_FIELDS`
|
|
307
|
+
(ip / city / country name / ASN org / ISP). There is no rule text on this model.
|
|
308
|
+
- Batch **Unblock** and **Whitelist** collect one reason per batch through
|
|
309
|
+
`prepare`; whitelist sends the DICT form `{reason, until}` so the audit trail
|
|
310
|
+
and `whitelisted_until` are both real (the source sent a bare string).
|
|
311
|
+
|
|
312
|
+
## Exemptions and the full-replace race
|
|
313
|
+
|
|
314
|
+
`POST /api/geo/allowlist` replaces the whole `GEOFENCE_ALLOWLIST` setting, so a
|
|
315
|
+
stale local copy silently deletes a concurrent editor's entry. Every write here
|
|
316
|
+
**refetches and compares first** and aborts with an explanatory message when the
|
|
317
|
+
server list has moved (`AllowlistRaceError`). An empty list is a legitimate
|
|
318
|
+
CLEAR, not "no change" — removing the last entry asks explicitly.
|
|
319
|
+
|
|
320
|
+
Expired entries are listed with `active: false` and rendered inactive, never
|
|
321
|
+
hidden: an auditor needs to see that an exemption existed.
|
|
322
|
+
|
|
323
|
+
Removing an individual IP whitelist resolves the record through a **cached read**
|
|
324
|
+
(`?ip_address=`), not `/geoip/lookup` — the row exists by definition, so removal
|
|
325
|
+
costs no provider call and no rate-limit budget. Adding one does use `/lookup`,
|
|
326
|
+
because the address may never have been seen.
|
|
327
|
+
|
|
328
|
+
## Degrading on a backend without the config plane
|
|
329
|
+
|
|
330
|
+
`/api/geo/*` is simply **not registered** on an older django-mojo, so the wire
|
|
331
|
+
answer is a **404**, not an error envelope. `isGeofenceApiMissing(error)` is the
|
|
332
|
+
branch; the copy names the documented floor (v1.2.42) as prose and asserts no
|
|
333
|
+
version the client can verify — there is no version endpoint. Any other status
|
|
334
|
+
shows the server's own message.
|
|
335
|
+
|
|
336
|
+
## Armed confirmation
|
|
337
|
+
|
|
338
|
+
Anything that reaches the fleet is an `ArmedButton` whose armed label names the
|
|
339
|
+
blast radius: the rules replace (with a plain-language clause diff), override
|
|
340
|
+
removal, ipset enable / disable / sync / refresh / delete, allowlist removal and
|
|
341
|
+
whitelist removal. Operations that need input (a reason, an expiry) use
|
|
342
|
+
`formModal` instead — an armed button cannot collect input.
|
|
343
|
+
|
|
344
|
+
## Absent by design
|
|
345
|
+
|
|
346
|
+
Do not add these back.
|
|
347
|
+
|
|
348
|
+
1. **Batch delete of IP sets.** Multi-selecting kernel firewall sets and deleting
|
|
349
|
+
them in one irreversible action is exactly the shape #1097 warned about.
|
|
350
|
+
Delete is single-record and armed, from the detail modal.
|
|
351
|
+
2. **Creating a block from the Blocked IPs table.** The backend supports it and
|
|
352
|
+
web-mojo did not expose it either. Unblock and whitelist — both *relaxing* —
|
|
353
|
+
are exposed.
|
|
354
|
+
3. **Editing geofence posture.** `GEOFENCE_ENABLED`, `GEOFENCE_FAIL_CLOSED`,
|
|
355
|
+
`GEOFENCE_FAIL_CLOSED_SCOPES`, `GEOFENCE_CACHE_TTL` and
|
|
356
|
+
`GEOFENCE_STRICT_POSTURE` are plain `Setting` rows with no validated
|
|
357
|
+
geofence-specific endpoint. The header is read-only and points at Runtime
|
|
358
|
+
Settings.
|
|
359
|
+
4. **Granting or revoking `bypass_geofence`.** The backend's own comment says
|
|
360
|
+
grants are managed on user records, and the endpoint returns id/username only
|
|
361
|
+
to avoid leaking user PII through a geofence-only grant.
|
|
362
|
+
5. **A world map / choropleth.** Owned by #1426 and reserved. The ranked country
|
|
363
|
+
list ships instead and is the same server-derived dataset.
|
|
364
|
+
6. **A general GeoLocatedIP administration table.** That is #1291's IP
|
|
365
|
+
Intelligence section; the Blocked IP modal links to its dossier.
|
|
366
|
+
7. **`geoip_sync` / federation controls.** Fleet-peer plumbing, not perimeter
|
|
367
|
+
operation.
|
|
368
|
+
8. **Event deletion or editing from the blocks log.** The evidence plane is
|
|
369
|
+
append-only by design.
|
|
370
|
+
9. **A client-side "top blocked countries" computed from the visible page.** It
|
|
371
|
+
would lie about totals whenever the page did not cover the window.
|
|
372
|
+
|
|
373
|
+
## Verification
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
node scripts/verify-admin-network.mjs # or: npm run verify:admin-network
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Headless assertions over the nine permission clauses, section registration and
|
|
380
|
+
route generation for both mounts, the full `geofence-data` mapping table
|
|
381
|
+
(`isAdvancedRule` truth table, form round trip, `buildGroupRulePayload` null-out
|
|
382
|
+
and sub-key allowlist, `coerceRuleInput`, `collectScopes`, `describeDecision`
|
|
383
|
+
over every `_DETAIL_MAP` code, `describeWouldBlock` including the nullable
|
|
384
|
+
shadow, `buildSimulateBody`, `diffRules`), the editor's two refusals, the app
|
|
385
|
+
shim proving ONE module by identity, the source-shape rules (no route/right-panel
|
|
386
|
+
inspection, `modal.detail` present, no `is_enabled` write, no batch delete, no
|
|
387
|
+
block creation, a read-only posture header), the persona 403 matrix across
|
|
388
|
+
`/api/geo/*`, `/api/incident/ipset` and `/api/system/geoip`, and the mock wire:
|
|
389
|
+
graph exclusions, the cache-only 400, the create-disabled default, the
|
|
390
|
+
list-vs-string `data` trap, name uniqueness, delete-perm narrowness, DSL and
|
|
391
|
+
allowlist validation, the empty-list clear, top-level `enabled`, the exempt
|
|
392
|
+
shadow fields, the firewall Log trail with its per-kind payload shape, and the
|
|
393
|
+
flat `category_slugs` envelope. Plus theme byte-identity and tokens-only CSS.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Phone Hub admin (#1295)
|
|
2
|
+
|
|
3
|
+
Import `PhoneHubPage`, the three individual pages, models, permission clauses, and API helpers from `portal-mojo/admin`. `PHONE_HUB_ADMIN_SECTION` registers one global `/phonehub` route in Communications; the page exposes only the Phone Numbers, SMS, and Provider Config tabs the caller may view. Admin global scope masks any outer product group.
|
|
4
|
+
|
|
5
|
+
## Wire contracts
|
|
6
|
+
|
|
7
|
+
- `POST /api/phonehub/number/normalize` is public. `normalizePhoneNumber()` accepts the backend result only when it is strict E.164.
|
|
8
|
+
- `POST /api/phonehub/number/lookup` requires authentication and is offered only under `PHONE_LOOKUP_UI_PERMISSIONS`. The helper always normalizes first. For a new or expired record it omits `force_refresh` so the backend's normal lookup path refreshes once. For a known-fresh record it requires confirmation, then sends `force_refresh: true`. A successful response must provide a first lookup timestamp/count or advance one of them relative to the prior row; a 2xx stale row is rejected as provider failure evidence.
|
|
9
|
+
- `PhoneNumberModel` requests the default graph but positively sanitizes every row before Query. Raw `lookup_data` is never retained or exported.
|
|
10
|
+
- `SmsModel` is an audit-only surface. It requests the default graph and positively projects known display fields before Query; raw metadata, provider payloads, and provider message ids are discarded. Its client exporter is bounded and omits message bodies, errors, user/group details, and raw fields. There is no send UI.
|
|
11
|
+
- `PhoneConfigModel` reads sanitized scalar/default-graph rows. Saves and `test_connection` use imperative, cache-free calls. Secret inputs are DOM refs, not React or Query state: blank means untouched and is omitted; replacement must be non-empty; clear sends `null` only after a separate confirmation. Responses are allowlisted before cache reconciliation. This surface edits existing provider credentials; it does not provision Django user/group API keys.
|
|
12
|
+
|
|
13
|
+
## Exact permissions
|
|
14
|
+
|
|
15
|
+
Every exported array is an ANY-of clause and retains the `sys.` UI namespace. Number view is `view_phone_numbers | manage_phone_numbers | comms | manage_users`; save is `manage_phone_numbers | comms | manage_users`; delete is only `manage_phone_numbers`. SMS view is `view_sms | manage_sms | comms | owner | manage_notifications`; save is `manage_sms | comms | manage_notifications`; delete is `manage_sms | manage_notifications`. Config view/save is `manage_phone_config | manage_groups | comms`; delete is `manage_phone_config | manage_groups`. The server remains authoritative.
|
|
16
|
+
|
|
17
|
+
Group choices are fetched only with a separate global group-directory clause and are capped at 100 basic rows. Without that grant the editor does not issue a group-directory request and preserves the existing scope (or creates a system default).
|
|
18
|
+
|
|
19
|
+
## Provider and deletion reality
|
|
20
|
+
|
|
21
|
+
Phone lookup is always the global Twilio Lookup integration; it is not group-configured. Stored Twilio and AWS credentials can be tested, but the current SMS send implementation only consults a per-group configuration for `provider="mojo"`. Twilio/AWS configurations otherwise fall through to global Twilio send settings, and AWS sending is not wired. `test_mode` only short-circuits connection testing; it does not block sending.
|
|
22
|
+
|
|
23
|
+
An active group config wins; otherwise `PhoneConfig.get_for_group` falls back to the first active system default. Deleting or deactivating a group config restores that fallback. Deleting a group cascades its config and SMS rows. Deleting a user cascades their SMS rows. Deleting a config does not delete SMS audit rows. Deleting an SMS row affects only local audit storage and cannot recall provider delivery.
|
|
24
|
+
|
|
25
|
+
## Reuse rules
|
|
26
|
+
|
|
27
|
+
Use KISS detail modals, not `:id` routes. Never add provider payloads to a model graph, cache, error, export, or request history. Never reuse `PhoneConfigModel.useSave()` for credentials. Do not add SMS sending or API-key provisioning to this admin area without a new reviewed contract.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Push Notifications admin (#1296)
|
|
2
|
+
|
|
3
|
+
Import `PushPage`, its individual panels/pages, the canonical push models, API helpers, and `PUSH_ADMIN_SECTION` from `portal-mojo/admin`. The section contributes one global/no-group Communications route at `/push`; there are no record routes. KISS detail modals own record inspection.
|
|
4
|
+
|
|
5
|
+
## Tabs and URL state
|
|
6
|
+
|
|
7
|
+
The page has six possible tabs: Your stats, Global metrics, Devices, Deliveries, Templates, and Config. Stats are authenticated caller data. Every other tab is independently filtered by its exact permission clause, and only the active authorized panel mounts, so hidden/inactive surfaces issue no requests.
|
|
8
|
+
|
|
9
|
+
`push_surface` is registered with `registerNonFilterParams` at module load. It survives table writes but never becomes a Django lookup or filter pill. Changing tabs replaces the flat URL params with only `push_surface`, intentionally clearing search, sort, paging, and filters that belong to the prior model.
|
|
10
|
+
|
|
11
|
+
## Exact permissions
|
|
12
|
+
|
|
13
|
+
All UI clauses carry the `sys.` namespace and are ANY-of:
|
|
14
|
+
|
|
15
|
+
- Devices view: `view_devices | manage_devices | comms | owner | manage_users`; save: `manage_devices | comms | owner`.
|
|
16
|
+
- Deliveries view: `view_notifications | manage_notifications | comms | owner | manage_users`; save: `manage_notifications | comms`.
|
|
17
|
+
- Templates view: `manage_notifications | manage_groups | comms | owner | manage_users`; save: `manage_notifications | manage_groups | comms`.
|
|
18
|
+
- Config view/save: `manage_push_config | manage_groups | comms`.
|
|
19
|
+
- Config connection test: `manage_push_config | comms`.
|
|
20
|
+
- Global metrics: `view_metrics | metrics`, with `account="global"`.
|
|
21
|
+
|
|
22
|
+
`sys.users` is deliberately absent: it is not in `RegisteredDevice.RestMeta.VIEW_PERMS`. The Stats endpoint requires authentication only and always scopes all five keys to `request.user`.
|
|
23
|
+
|
|
24
|
+
## Projection and ownership boundaries
|
|
25
|
+
|
|
26
|
+
Every model pins a `basic` or `default` graph, allowlists list params, and positively projects rows before Query or Mutation cache reconciliation. RegisteredDevice never retains `device_token`; Delivery never retains `platform_data`, and its arbitrary `data_payload` is not retained by the UI projection. Related users are reduced to id plus a short display label—never email, phone, metadata, or an expanded user graph. Template JSON objects accept only a bounded shallow scalar projection on reads.
|
|
27
|
+
|
|
28
|
+
`PushDeviceModel` now lives canonically in `admin/messaging/push`. The former `admin/identity/users/models.ts` definition is a compatibility re-export of the same object, so User Detail and the global Push page share one endpoint definition and Query key.
|
|
29
|
+
|
|
30
|
+
## FCM credentials and actions
|
|
31
|
+
|
|
32
|
+
FCM writes are imperative and cache-free. A service account textarea is an uncontrolled DOM ref and never prefills. Blank means untouched and is omitted. Replacement must parse to an object with `type="service_account"`, `project_id`, `client_email`, and a PEM `private_key`. Clear sends `null` only after explicit confirmation. Empty replacement is impossible, and server responses are sanitized before cache reconciliation.
|
|
33
|
+
|
|
34
|
+
Connection testing calls only `POST /api/account/devices/push/config/<id>/test` with an empty body. The backend uses its dummy token to validate credentials. The portal never accepts a device token. It also does not expose `/send`, caller `/test`, delivery retry, device registration/unregistration, or any delete action.
|
|
35
|
+
|
|
36
|
+
All four REST models have live `CAN_DELETE = false`; the mock returns 403 without removing or cascading anything. Templates/configs can be created and edited under their save clauses. Devices and deliveries remain investigative here.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Rule Engine admin
|
|
2
|
+
|
|
3
|
+
Import models, routes, transforms, and the builder from `portal-mojo/admin`:
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import {
|
|
7
|
+
RuleSetModel, RuleModel, HandlerChainBuilder,
|
|
8
|
+
parseHandlerChain, runtimeEffectiveHandlerChain,
|
|
9
|
+
} from 'portal-mojo/admin';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The shared Security Operations section mounts the `rules` table after Tickets,
|
|
13
|
+
Incidents, and Events. Selecting a RuleSet opens its DetailView through the
|
|
14
|
+
shared KISS `modal.detail` surface; there is no `rules/:id` child route, so the
|
|
15
|
+
table URL and query state remain unchanged. The mount-relative table works at
|
|
16
|
+
`#/security/rules` in the standalone Admin and `#/system/security/rules` when
|
|
17
|
+
embedded. Reads require
|
|
18
|
+
`sys.view_security|sys.security`; mutations require
|
|
19
|
+
`sys.manage_security|sys.security`. These are system-pinned clauses: active
|
|
20
|
+
group grants do not satisfy them, and the server remains authoritative.
|
|
21
|
+
|
|
22
|
+
## Wire models and safety boundary
|
|
23
|
+
|
|
24
|
+
`RuleSetModel` uses `/api/incident/event/ruleset`; `RuleModel` uses
|
|
25
|
+
`/api/incident/event/ruleset/rule`. Row types intentionally remain open and
|
|
26
|
+
fetched rows are not reconstructed, so future top-level fields, metadata,
|
|
27
|
+
enum values, comparators, and value types remain in TanStack Query unchanged.
|
|
28
|
+
|
|
29
|
+
Rule-set creation always sends `is_active:false`, regardless of form input.
|
|
30
|
+
An empty active RuleSet is a category catch-all and can preempt later
|
|
31
|
+
priorities, so enabling a zero-condition set is a separate warning that names
|
|
32
|
+
its category and priority. The create defaults are priority 10, ALL matching,
|
|
33
|
+
SOURCE_IP bundling for 30 minutes, and an empty handler. `bundle_minutes=0`
|
|
34
|
+
disables time bundling; `null` means the current backend's unbounded window.
|
|
35
|
+
Count thresholds require effective bundling, trigger/retrigger windows require
|
|
36
|
+
a count, and a finite trigger window cannot exceed the finite bundle window.
|
|
37
|
+
|
|
38
|
+
Rule transforms reject leading-underscore fields and validate integer/float
|
|
39
|
+
comparison values. Existing `bool` rules are visible and preservable, but
|
|
40
|
+
creating or changing a bool comparison is blocked: the backend currently
|
|
41
|
+
applies Python `bool()` to stored strings, so the string `"false"` is true.
|
|
42
|
+
|
|
43
|
+
## Handler grammar and preservation
|
|
44
|
+
|
|
45
|
+
Supported runtime schemes are `job`, `email`, `sms`, `notify`, `block`,
|
|
46
|
+
`ticket`, `maestro`, `llm`, and `resolve`. Notification targets may contain
|
|
47
|
+
commas (`perm@…`, `protected@…`, usernames). The editor recognizes a chain
|
|
48
|
+
boundary only at comma + URI scheme, preserving target commas.
|
|
49
|
+
|
|
50
|
+
There are deliberately two projections:
|
|
51
|
+
|
|
52
|
+
- The editor projection exposes any URI scheme so unsupported content is
|
|
53
|
+
visible and movable/removable only by explicit operator action.
|
|
54
|
+
- The runtime-effective projection exactly mirrors django-mojo's split:
|
|
55
|
+
comma immediately followed by a *known* scheme. Leading unknown segments
|
|
56
|
+
are skipped; unknown or whitespace-prefixed segments after a known handler
|
|
57
|
+
can be swallowed into that handler's spec. The builder warns about both.
|
|
58
|
+
|
|
59
|
+
Every step retains its raw bytes and preceding separator. Parsing then
|
|
60
|
+
serializing an untouched chain is byte-for-byte exact. Structured changes are
|
|
61
|
+
surgical: ordered query entries and unknown parameters remain, duplicate keys
|
|
62
|
+
remain, and the first duplicate is the runtime-effective value that edits
|
|
63
|
+
target. Moving/editing/removing skipped or swallowed content requires an
|
|
64
|
+
explicit high-severity behavior-change confirmation. An empty string is valid:
|
|
65
|
+
the incident is recorded and no action is dispatched.
|
|
66
|
+
|
|
67
|
+
Handler order is publication/dispatch order. Jobs run asynchronously, so the
|
|
68
|
+
UI does not promise completion order. Structural validation covers URI shape,
|
|
69
|
+
percent encoding, required targets/module paths, integer/range values, blank
|
|
70
|
+
parameter keys, and resolve statuses. It cannot prove target or permission
|
|
71
|
+
existence, job importability, Maestro board existence, runtime settings, or
|
|
72
|
+
Python regex semantics; failed server mutations reject visibly and leave the
|
|
73
|
+
editor open.
|
|
74
|
+
|
|
75
|
+
## Reordering limits
|
|
76
|
+
|
|
77
|
+
The whole handler chain is one RuleSet text field, so a chain edit/reorder is
|
|
78
|
+
one atomic save. RuleSet priorities and child Rule indexes are separate rows;
|
|
79
|
+
django-mojo exposes no atomic multi-row reorder action and `CAN_BATCH` is
|
|
80
|
+
false. Drag reorder is therefore absent. Operators edit one numeric value at
|
|
81
|
+
a time, and duplicate priorities/indexes are flagged because tie order is
|
|
82
|
+
undefined.
|
|
83
|
+
|
|
84
|
+
The showcase demo is under **Admin → Rule Engine** and includes supported
|
|
85
|
+
steps, notification target commas, duplicate/extra parameters, unsupported
|
|
86
|
+
content, structural errors, move/remove actions, exact raw serialization, and
|
|
87
|
+
the empty-chain case.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Admin Security Tickets
|
|
2
|
+
|
|
3
|
+
`SECURITY_OPERATIONS_ADMIN_SECTION` adds the mount-relative `security/tickets` route to the shared admin registry. Consumers normally mount it through `ADMIN_SECTIONS`; direct imports are available from `portal-mojo/admin`.
|
|
4
|
+
|
|
5
|
+
## Permissions
|
|
6
|
+
|
|
7
|
+
The section and route use `sys.view_security` or `sys.security`. Mutating controls require `sys.manage_security` or `sys.security`; delete uses `sys.manage_security`. Assignee lookup is offered only with `sys.view_users`, `sys.manage_users`, or `sys.users`. These are UI gates only—the server remains authoritative for every request.
|
|
8
|
+
|
|
9
|
+
## Wire contracts
|
|
10
|
+
|
|
11
|
+
- Ticket collection/detail: `/api/incident/ticket`. List state uses the standard Django lookups, graph relations, paging, sorting, search, and `download_format=csv|json`. Saves accept title, description, arbitrary status/category strings, integer priority 1–10, and a nullable assignee ID.
|
|
12
|
+
- Notes: `/api/incident/ticket/note`, always scoped with the ticket `parent` and, for grouped tickets, the ticket's own `group`. Operator notes require text and may atomically add one completed existing File id as `media`. Responses expose only File's reference graph. A status save creates a structured note with `metadata.type=status_change`, `old_status`, and `new_status`.
|
|
13
|
+
- Ticket actions: detail POST bodies use `enable_llm`, `disable_llm`, or `push_to_maestro`. Push success means queued, not linked.
|
|
14
|
+
- Maestro links: `/api/incident/maestro/item-link?ticket=<id>`. The detail modal polls for at most 30 seconds after a queued push and always offers a manual check.
|
|
15
|
+
|
|
16
|
+
Unknown status/category values are appended to the known UI options and are never coerced away. Relation values may be a graph object, a bare ID, or null.
|
|
17
|
+
|
|
18
|
+
## Cache and approval boundaries
|
|
19
|
+
|
|
20
|
+
Every ticket mutation invalidates the Ticket model root, the record-scoped TicketNote feed prefix, and Maestro link lists. Approval and denial are new TicketNote rows with `metadata.action_response`. The response copies the pending action's `handler` and `context` without reconstructing or exposing editable JSON. Unknown handlers render a generic informational action card; the client never executes handler-specific navigation or code.
|
|
21
|
+
|
|
22
|
+
Resolved actions, terminal tickets, missing manage permission, and in-flight responses disable approval controls. The server still owns replay protection and handler authorization.
|
|
23
|
+
|
|
24
|
+
## Detail modal behavior
|
|
25
|
+
|
|
26
|
+
Ticket titles are real buttons and both title clicks and row clicks open the
|
|
27
|
+
shared KISS `modal.detail` surface without changing the current route or table
|
|
28
|
+
params. The modal fetches the authoritative detail row and uses `DetailView`
|
|
29
|
+
sections for Overview, permission-gated Manage controls, and Activity.
|
|
30
|
+
Nested edit and confirmation modals use the native dialog stack, and closing
|
|
31
|
+
the detail returns to the unchanged queue. `RecordFeed` uses compact mode and
|
|
32
|
+
receives `showInput={canManage}`.
|
|
33
|
+
|
|
34
|
+
Activity notes offer the shared capacity-one upload queue only while manage
|
|
35
|
+
permission and an authoritative ticket/group are current. Grouped uploads use
|
|
36
|
+
the ticket's group, groupless uploads use personal storage, and the completed
|
|
37
|
+
manager/group lifecycle response must match before `media` can be sent. Queue
|
|
38
|
+
removal never deletes File. Assistant chat and incident-detail navigation
|
|
39
|
+
remain separate, and a remote Maestro URL becomes a link only when it parses as
|
|
40
|
+
HTTP or HTTPS.
|