Let only admins change the global settings
BINDERY_ADMIN_USERS lists the admins of the cwa and proxy modes. Only an admin can change the global settings and the global rules, and read or change personal.ini. Without a list, each user is an admin.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { find, install, server, settle } from './flow-helpers.mjs';
|
||||
|
||||
const ui = install();
|
||||
const { stopPolling } = await import('../../bindery/static/js/jobs.js');
|
||||
|
||||
test('a user who is not an admin gets no settings link and no settings page', async (t) => {
|
||||
t.after(stopPolling);
|
||||
const backend = server(t);
|
||||
backend.on('GET', 'auth/check', { authenticated: true, mode: 'proxy', user: 'bob', admin: false });
|
||||
backend.on('GET', 'config', { metadata_fields: {}, mode_labels: {}, library_available: false });
|
||||
backend.on('GET', 'jobs', []);
|
||||
backend.on('GET', 'stories', []);
|
||||
await import('../../bindery/static/js/main.js');
|
||||
await settle();
|
||||
assert.ok(find(ui.app, 'a', 'Stories'));
|
||||
assert.equal(find(ui.app, 'a', 'Settings'), undefined);
|
||||
window.location.hash = '#/settings';
|
||||
ui.windowEvents.hashchange();
|
||||
await settle();
|
||||
assert.match(ui.app.textContent, /Only an admin can open the settings/);
|
||||
// The page asks the server for no settings and no rules.
|
||||
assert.deepEqual(backend.sent('GET', 'settings'), []);
|
||||
assert.deepEqual(backend.sent('GET', 'rules'), []);
|
||||
});
|
||||
@@ -71,6 +71,7 @@ def test_cwa_login(client, cwa):
|
||||
"authenticated": False,
|
||||
"mode": "cwa",
|
||||
"user": None,
|
||||
"admin": False,
|
||||
"logout_url": None,
|
||||
}
|
||||
assert login(client, password="wrong").status_code == 401
|
||||
@@ -170,10 +171,54 @@ def test_proxy_header(client, monkeypatch):
|
||||
"authenticated": True,
|
||||
"mode": "proxy",
|
||||
"user": "alice",
|
||||
"admin": True,
|
||||
"logout_url": "https://auth.example.com/logout",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("user", "admin"), [("alice", True), ("Alice", True), ("bob", False)])
|
||||
def test_only_an_admin_changes_the_global_settings(client, monkeypatch, tmp_path, user, admin):
|
||||
monkeypatch.setattr(config, "PERSONAL_INI", tmp_path / "personal.ini")
|
||||
monkeypatch.setattr(config, "AUTH_METHOD", "proxy")
|
||||
monkeypatch.setattr(config, "ADMIN_USERS", ["alice"])
|
||||
sid = orchestrator.store.create_story("https://www.royalroad.com/fiction/1", "www.royalroad.com", "S", "W", [])
|
||||
headers = {**H, "X-Remote-User": user}
|
||||
status = 200 if admin else 403
|
||||
assert client.get("/api/auth/check", headers=headers).json["admin"] is admin
|
||||
assert ("personal_ini" in client.get("/api/settings", headers=headers).json) is admin
|
||||
for data in ({"recent_chapters": 5}, {"personal_ini": "[defaults]\n"}):
|
||||
assert client.put("/api/settings", json=data, headers=headers).status_code == status
|
||||
assert (orchestrator.get_settings()["recent_chapters"] == 5) is admin
|
||||
assert config.PERSONAL_INI.exists() is admin
|
||||
rules = {"rules": [{"pattern": "a", "replacement": "b"}]}
|
||||
response = client.put("/api/rules", json=rules, headers=headers)
|
||||
assert response.status_code == status
|
||||
assert bool(orchestrator.store.rules(None)) is admin
|
||||
if not admin:
|
||||
assert response.json == {"error": "Only an admin can do this. BINDERY_ADMIN_USERS lists the admins."}
|
||||
# Each user can read the global rules, and change the rules and the settings of a story.
|
||||
assert client.get("/api/rules", headers=headers).status_code == 200
|
||||
assert client.put(f"/api/stories/{sid}/rules", json=rules, headers=headers).status_code == 200
|
||||
assert client.put(f"/api/stories/{sid}/settings", json={"recheck_days": 3}, headers=headers).status_code == 200
|
||||
|
||||
|
||||
def test_the_api_key_and_single_user_modes_are_admins(client, monkeypatch):
|
||||
monkeypatch.setattr(config, "ADMIN_USERS", ["alice"])
|
||||
assert client.get("/api/auth/check").json["admin"] # the none mode has no users
|
||||
monkeypatch.setattr(config, "AUTH_METHOD", "proxy")
|
||||
monkeypatch.setattr(config, "API_KEY", "secret")
|
||||
key = {"X-Api-Key": "secret"}
|
||||
assert client.get("/api/auth/check", headers=key).json["admin"]
|
||||
assert client.put("/api/settings", json={"recent_chapters": 2}, headers=key).status_code == 200
|
||||
assert auth.is_admin(auth.Identity("bob", "proxy")) is False
|
||||
monkeypatch.setattr(config, "AUTH_METHOD", "local")
|
||||
assert auth.is_admin(auth.Identity("bob", "session"))
|
||||
# Without a list, each user is an admin.
|
||||
monkeypatch.setattr(config, "AUTH_METHOD", "proxy")
|
||||
monkeypatch.setattr(config, "ADMIN_USERS", [])
|
||||
assert auth.is_admin(auth.Identity("bob", "proxy"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trusted", "address", "accepted"),
|
||||
[
|
||||
|
||||
@@ -69,6 +69,7 @@ def test_values_from_the_environment(monkeypatch, reload_config):
|
||||
monkeypatch.setenv("UPDATE_INTERVAL_HOURS", "1.5")
|
||||
monkeypatch.setenv("DEFAULT_LOCKED_FIELDS", "title, authors")
|
||||
monkeypatch.setenv("CWA_ALLOWED_USERS", "alice,bob")
|
||||
monkeypatch.setenv("BINDERY_ADMIN_USERS", "alice")
|
||||
monkeypatch.setenv("CONFIG_DIR", "/data/bindery")
|
||||
monkeypatch.setenv("TRUST_PROXY", "yes")
|
||||
monkeypatch.setenv("CWA_DB_PATH", " /cwa/app.db ")
|
||||
@@ -83,6 +84,7 @@ def test_values_from_the_environment(monkeypatch, reload_config):
|
||||
assert config.UPDATE_INTERVAL_HOURS == 1.5
|
||||
assert config.DEFAULT_LOCKED_FIELDS == ["title", "authors"]
|
||||
assert config.CWA_ALLOWED_USERS == ["alice", "bob"]
|
||||
assert config.ADMIN_USERS == ["alice"]
|
||||
assert str(config.DB_PATH) == "/data/bindery/bindery.db"
|
||||
assert config.TRUST_PROXY is True
|
||||
assert str(config.CWA_DB_PATH) == "/cwa/app.db"
|
||||
@@ -101,6 +103,7 @@ def test_defaults(monkeypatch, reload_config):
|
||||
"PROXY_AUTH_USER_HEADER",
|
||||
"PROXY_AUTH_TRUSTED_IPS",
|
||||
"ALLOWED_HOSTS",
|
||||
"BINDERY_ADMIN_USERS",
|
||||
"SESSION_COOKIE_SECURE",
|
||||
"TRUST_PROXY",
|
||||
"CWA_DB_PATH",
|
||||
@@ -127,6 +130,7 @@ def test_defaults(monkeypatch, reload_config):
|
||||
"fe80::/10",
|
||||
]
|
||||
assert config.ALLOWED_HOSTS == []
|
||||
assert config.ADMIN_USERS == []
|
||||
assert config.SESSION_COOKIE_SECURE is False
|
||||
assert config.TRUST_PROXY is False
|
||||
assert config.CWA_DB_PATH is None
|
||||
|
||||
@@ -37,6 +37,7 @@ def test_local_login_logout_and_password_hashes(client, local, monkeypatch, meth
|
||||
"authenticated": False,
|
||||
"mode": "local",
|
||||
"user": None,
|
||||
"admin": False,
|
||||
"logout_url": None,
|
||||
}
|
||||
assert client.get("/api/stories").status_code == 401
|
||||
|
||||
Reference in New Issue
Block a user