feat(users): users.json v2 JSON Schema + validation tests (ref #120)

This commit is contained in:
CyberMind-FR 2026-05-13 08:37:03 +02:00
parent a2507c6559
commit 47fcaa6584
2 changed files with 151 additions and 0 deletions

View File

@ -0,0 +1,74 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://secubox.in/schemas/users-v2.json",
"title": "SecuBox Users v2",
"type": "object",
"required": ["version", "users", "groups"],
"additionalProperties": false,
"properties": {
"$schema": {"type": "string"},
"version": {"const": 2},
"users": {
"type": "array",
"items": {"$ref": "#/definitions/user"}
},
"groups": {"type": "array"}
},
"definitions": {
"user": {
"type": "object",
"required": [
"username", "role", "enabled", "must_change_password",
"services", "created"
],
"additionalProperties": false,
"properties": {
"username": {"type": "string", "pattern": "^[a-z0-9_-]{2,32}$"},
"email": {"type": ["string", "null"], "format": "email"},
"role": {"enum": ["admin", "operator", "viewer"]},
"enabled": {"type": "boolean"},
"password_hash": {"type": ["string", "null"]},
"must_change_password": {"type": "boolean"},
"totp": {"oneOf": [{"type": "null"}, {"$ref": "#/definitions/totp"}]},
"google": {"oneOf": [{"type": "null"}, {"$ref": "#/definitions/google"}]},
"services": {"type": "array", "items": {"type": "string"}},
"created": {"type": "string", "format": "date-time"},
"last_login": {"type": ["string", "null"], "format": "date-time"},
"provision_results": {"type": "object"}
}
},
"totp": {
"type": "object",
"required": ["secret", "enabled", "enrolled_at", "backup_codes"],
"additionalProperties": false,
"properties": {
"secret": {"type": "string", "minLength": 16},
"enabled": {"type": "boolean"},
"enrolled_at": {"type": "string", "format": "date-time"},
"last_step": {"type": ["integer", "null"]},
"backup_codes": {
"type": "array",
"items": {
"type": "object",
"required": ["hash", "used_at"],
"additionalProperties": false,
"properties": {
"hash": {"type": "string"},
"used_at": {"type": ["string", "null"], "format": "date-time"}
}
}
}
}
},
"google": {
"type": "object",
"required": ["sub", "email", "linked_at"],
"additionalProperties": false,
"properties": {
"sub": {"type": "string"},
"email": {"type": "string", "format": "email"},
"linked_at": {"type": "string", "format": "date-time"}
}
}
}
}

View File

@ -0,0 +1,77 @@
"""Validate users.json v2 against the JSON Schema."""
import json
from pathlib import Path
import jsonschema
import pytest
SCHEMA = json.loads(
(Path(__file__).resolve().parents[1] / "schema" / "users.json.schema.json").read_text()
)
def _doc(**overrides):
base = {
"version": 2,
"users": [
{
"username": "admin",
"email": "admin@example.local",
"role": "admin",
"enabled": True,
"password_hash": None,
"must_change_password": True,
"totp": None,
"google": None,
"services": [],
"created": "2026-05-13T00:00:00+00:00",
"last_login": None,
}
],
"groups": [],
}
base.update(overrides)
return base
def test_minimal_valid_doc():
jsonschema.validate(_doc(), SCHEMA)
def test_version_must_be_2():
with pytest.raises(jsonschema.ValidationError):
jsonschema.validate(_doc(version=1), SCHEMA)
def test_username_pattern_enforced():
doc = _doc()
doc["users"][0]["username"] = "BadName!"
with pytest.raises(jsonschema.ValidationError):
jsonschema.validate(doc, SCHEMA)
def test_role_enum_enforced():
doc = _doc()
doc["users"][0]["role"] = "superuser"
with pytest.raises(jsonschema.ValidationError):
jsonschema.validate(doc, SCHEMA)
def test_totp_block_shape():
doc = _doc()
doc["users"][0]["totp"] = {
"secret": "JBSWY3DPEHPK3PXP",
"enabled": True,
"enrolled_at": "2026-05-13T00:00:00+00:00",
"last_step": None,
"backup_codes": [
{"hash": "$argon2id$...", "used_at": None}
],
}
jsonschema.validate(doc, SCHEMA)
def test_password_hash_may_be_null():
doc = _doc()
doc["users"][0]["password_hash"] = None
jsonschema.validate(doc, SCHEMA)