{
- Router::new().route("/login", get(logic::login_page).post(logic::login))
+ Router::new()
+ .route("/login", get(logic::login_page).post(logic::login))
+ .route(
+ "/initial-password",
+ get(logic::initial_password_page).post(logic::set_initial_password),
+ )
}
diff --git a/web/src/pages/login/state.rs b/web/src/pages/login/state.rs
index a02fc15c..955e2c94 100644
--- a/web/src/pages/login/state.rs
+++ b/web/src/pages/login/state.rs
@@ -4,3 +4,18 @@ pub(crate) struct LoginInput {
#[serde(default)]
pub password: String,
}
+
+#[derive(Default, serde::Deserialize)]
+pub(crate) struct LoginQuery {
+ #[serde(default)]
+ pub permissions_changed: bool,
+ #[serde(default)]
+ pub initial_password_set: bool,
+}
+
+#[derive(serde::Deserialize)]
+pub(crate) struct InitialPasswordInput {
+ pub username: String,
+ pub password: String,
+ pub password_confirmation: String,
+}
diff --git a/web/src/pages/login/ui.rs b/web/src/pages/login/ui.rs
index b6efce77..ad9c0d03 100644
--- a/web/src/pages/login/ui.rs
+++ b/web/src/pages/login/ui.rs
@@ -7,13 +7,37 @@ use crate::ui::{Alert, Nav, render};
#[template(path = "pages/login/login.html")]
struct LoginPage {
nav: Nav,
+ permissions_changed: bool,
+ initial_password_set: bool,
}
-pub(crate) fn render_page(nav: Nav) -> String {
- render(&LoginPage { nav })
+pub(crate) fn render_page(
+ nav: Nav,
+ permissions_changed: bool,
+ initial_password_set: bool,
+) -> String {
+ render(&LoginPage {
+ nav,
+ permissions_changed,
+ initial_password_set,
+ })
}
/// POST /login — the #login-status swap when the credentials are rejected.
pub(crate) fn render_error(message: &str) -> String {
render(&Alert::error("Could not sign in", message))
}
+
+#[derive(Template)]
+#[template(path = "pages/login/initial_password.html")]
+struct InitialPasswordPage {
+ nav: Nav,
+}
+
+pub(crate) fn render_initial_password_page(nav: Nav) -> String {
+ render(&InitialPasswordPage { nav })
+}
+
+pub(crate) fn render_initial_password_error(message: &str) -> String {
+ render(&Alert::error("Could not claim the bootstrap account", message))
+}
diff --git a/web/src/ui/mod.rs b/web/src/ui/mod.rs
index 30f94090..51f0cf7a 100644
--- a/web/src/ui/mod.rs
+++ b/web/src/ui/mod.rs
@@ -15,6 +15,9 @@ pub(crate) const SESSION_COOKIE: &str = "analytics_token";
pub(crate) struct Nav {
pub authenticated: bool,
pub role: String,
+ pub can_admin: bool,
+ pub can_import: bool,
+ pub can_export: bool,
pub active: &'static str,
}
@@ -25,14 +28,25 @@ impl Nav {
Self {
authenticated: crate::cookie_value(headers, SESSION_COOKIE).is_some(),
role: String::new(),
+ can_admin: false,
+ can_import: false,
+ can_export: false,
active,
}
}
- /// The admin page is the only one that learns the caller's role, so it is
- /// the only one that can show the role badge.
- pub(crate) fn with_role(mut self, role: String) -> Self {
- self.role = role;
+ pub(crate) fn with_authorization(
+ mut self,
+ authorization: &crate::auth::AuthorizationSnapshot,
+ ) -> Self {
+ self.role = authorization.role.clone();
+ self.can_admin = crate::authz::can_open_admin(authorization);
+ self.can_import = authorization.permissions.iter().any(|permission| {
+ permission.action == "insert" && permission.object.starts_with("data:")
+ });
+ self.can_export = authorization.permissions.iter().any(|permission| {
+ permission.action == "read" && permission.object.starts_with("data:")
+ });
self
}
}
@@ -42,6 +56,9 @@ impl Default for Nav {
Self {
authenticated: false,
role: String::new(),
+ can_admin: false,
+ can_import: false,
+ can_export: false,
active: "",
}
}
diff --git a/web/templates/pages/add_table/add_table.html b/web/templates/pages/add_table/add_table.html
index c690bfdb..d733003a 100644
--- a/web/templates/pages/add_table/add_table.html
+++ b/web/templates/pages/add_table/add_table.html
@@ -4,7 +4,7 @@
{% block title %}Add table{% endblock %}
{% block eyebrow %}Table definition{% endblock %}
{% block heading %}Add table{% endblock %}
-{% block lead %}Build the table one column at a time, the same way the terminal client does.
{% endblock %}
+{% block lead %}Build the table one column at a time. After creation, its table-definition workspace opens so you can assign the initial role permissions.
{% endblock %}
{% block form %}
{#
diff --git a/web/templates/pages/admin/admin/admin.html b/web/templates/pages/admin/admin/admin.html
index a19c164d..ce51673a 100644
--- a/web/templates/pages/admin/admin/admin.html
+++ b/web/templates/pages/admin/admin/admin.html
@@ -12,13 +12,13 @@
Browse profiles, tables, and their physical columns.
{% include "pages/admin/admin/workspace.html" %}
diff --git a/web/templates/pages/admin/permissions/permissions.html b/web/templates/pages/admin/permissions/permissions.html
new file mode 100644
index 00000000..1d0c6a86
--- /dev/null
+++ b/web/templates/pages/admin/permissions/permissions.html
@@ -0,0 +1,99 @@
+{% extends "ui/base.html" %}
+
+{% block title %}Roles and permissions{% endblock %}
+
+{% block content %}
+
+
+
+
Authorization
+
Roles and permissions
+
Manage data roles, their direct grants, inheritance, and user assignments.
+
+
+
+
+ {% if page.can_manage_roles %}
+
+ Data roles
+
+
+ {% if page.selected_role_is_removable() %}
+
+ {% endif %}
+
+
+
+ Grants for {{ page.selected_role }}
+ Direct grants can be revoked here. “Inherited” permissions come from the role’s parent.
+
+
+ {% endif %}
+
+ {% if page.can_manage_users %}
+
+ Users
+
+ | Username | Email | Current role | Assign role |
+ {% for user in page.users %}
+ | {{ user.username }} | {{ user.email }} | {{ user.role }} |
+ |
+
{% endfor %}
+
+
+ {% endif %}
+
+
+{% endblock %}
diff --git a/web/templates/pages/admin/table_definition/workspace.html b/web/templates/pages/admin/table_definition/workspace.html
index e43d94a7..8a711b8c 100644
--- a/web/templates/pages/admin/table_definition/workspace.html
+++ b/web/templates/pages/admin/table_definition/workspace.html
@@ -151,6 +151,35 @@
{% endif %}
+{% if !page.permission_object.is_empty() %}
+
+ Data permissions for {{ page.selection.table }}
+ These grants cover this table family. A successful change retires the current session, so you will be asked to sign in again.
+
+ | Role | Actions |
+ {% for role in page.role_permissions %}
+ | {{ role.role }} |
+ {% for permission in role.actions %}
+ {% if permission.direct %}
+
+ {% else if permission.effective %}
+ {{ permission.action }} · inherited
+ {% else %}
+
+ {% endif %}
+ {% endfor %} |
+
{% endfor %}
+
+
+
+{% endif %}
+
{% if page.table_is_writable() %}
{#
Append columns. The panel stages columns without writing anything; the
diff --git a/web/templates/pages/import_export/export/export.html b/web/templates/pages/import_export/export/export.html
index 0db147c3..bb1db9ab 100644
--- a/web/templates/pages/import_export/export/export.html
+++ b/web/templates/pages/import_export/export/export.html
@@ -27,7 +27,7 @@
{%- for profile in page.catalog.profiles %}{% for table in profile.tables %}{% endfor %}{% endfor -%}
diff --git a/web/templates/pages/import_export/import/import.html b/web/templates/pages/import_export/import/import.html
index 90c95ceb..3bf73f85 100644
--- a/web/templates/pages/import_export/import/import.html
+++ b/web/templates/pages/import_export/import/import.html
@@ -12,12 +12,7 @@
hx-disabled-elt="button[type=submit]">
+
@@ -33,7 +31,7 @@
{%- if let Some(message) = page.error %}{% call alert::error("Could not import CSV", message) %}{% endcall %}{% endif -%}
diff --git a/web/templates/pages/login/initial_password.html b/web/templates/pages/login/initial_password.html
new file mode 100644
index 00000000..00b9a65f
--- /dev/null
+++ b/web/templates/pages/login/initial_password.html
@@ -0,0 +1,20 @@
+{% extends "ui/base.html" %}
+
+{% block title %}Claim administrator{% endblock %}
+
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/web/templates/pages/login/login.html b/web/templates/pages/login/login.html
index beffd383..0506fe2a 100644
--- a/web/templates/pages/login/login.html
+++ b/web/templates/pages/login/login.html
@@ -8,10 +8,13 @@
diff --git a/web/templates/ui/navbar.html b/web/templates/ui/navbar.html
index f96e1b3d..b2e1768d 100644
--- a/web/templates/ui/navbar.html
+++ b/web/templates/ui/navbar.html
@@ -22,8 +22,10 @@
- - Admin
+ {% if nav.can_admin %}- Admin
{% endif %}
- Analytics
+ {% if nav.can_import %}- Import
{% endif %}
+ {% if nav.can_export %}- Export
{% endif %}
{% if nav.authenticated %}
{% else %}
@@ -42,8 +44,10 @@