diff --git a/roles/synapse/cfg.schema.json b/roles/synapse/cfg.schema.json new file mode 100644 index 0000000..47d54c1 --- /dev/null +++ b/roles/synapse/cfg.schema.json @@ -0,0 +1,343 @@ +{ + "nullable": false, + "type": "object", + "properties": { + "scheme": { + "nullable": false, + "type": "string", + "default": "https" + }, + "domain": { + "nullable": false, + "type": "string" + }, + "database": { + "anyOf": [ + { + "nullable": false, + "type": "object", + "properties": { + "kind": { + "nullable": false, + "type": "string", + "enum": ["sqlite"], + "default": "sqlite" + }, + "data": { + "nullable": false, + "type": "object", + "properties": { + "path": { + "nullable": false, + "type": "string", + "default": "/var/synapse/data.sqlite" + } + }, + "additionalProperties": false, + "required": [ + "path" + ] + } + }, + "additionalProperties": false, + "required": [ + "kind", + "data" + ] + }, + { + "nullable": false, + "type": "object", + "properties": { + "kind": { + "nullable": false, + "type": "string", + "enum": ["postgresql"], + "default": "postgresql" + }, + "data": { + "nullable": false, + "type": "object", + "properties": { + "host": { + "nullable": false, + "type": "string", + "default": "localhost" + }, + "port": { + "nullable": false, + "type": "integer", + "default": 5432 + }, + "username": { + "nullable": false, + "type": "string", + "default": "synapse_user" + }, + "password": { + "nullable": false, + "type": "string" + }, + "schema": { + "nullable": false, + "type": "string", + "default": "synapse" + } + }, + "additionalProperties": false, + "required": [ + "host", + "port", + "username", + "password", + "schema" + ] + } + }, + "additionalProperties": false, + "required": [ + "kind", + "data" + ] + } + ] + }, + "element_url": { + "nullable": true, + "type": "string", + "default": null + }, + "title": { + "nullable": false, + "type": "string", + "default": "Example | Matrix" + }, + "federation": { + "nullable": false, + "type": "object", + "properties": { + "enable": { + "nullable": false, + "type": "boolean", + "default": false + }, + "whitelist": { + "nullable": false, + "type": "array", + "items": { + "type": "string" + }, + "default": [] + } + }, + "additionalProperties": false, + "required": [ + "enable", + "whitelist" + ] + }, + "password_strict_policy": { + "nullable": false, + "type": "boolean", + "default": true + }, + "registration_shared_secret": { + "nullable": true, + "type": "string", + "default": null + }, + "authentication": { + "anyOf": [ + { + "nullable": false, + "type": "object", + "properties": { + "kind": { + "nullable": false, + "type": "string", + "enum": ["internal"], + "default": "internal" + }, + "data": { + "nullable": false, + "type": "object", + "properties": { + }, + "additionalProperties": false, + "required": [ + ] + } + }, + "additionalProperties": false, + "required": [ + "kind", + "data" + ] + }, + { + "nullable": false, + "type": "object", + "properties": { + "kind": { + "nullable": false, + "type": "string", + "enum": ["authelia"], + "default": "authelia" + }, + "data": { + "nullable": false, + "type": "object", + "properties": { + "provider_id": { + "nullable": false, + "type": "string", + "default": "authelia" + }, + "provider_name": { + "nullable": false, + "type": "string", + "default": "Authelia" + }, + "client_id": { + "nullable": false, + "type": "string", + "default": "synapse" + }, + "client_secret": { + "nullable": false, + "type": "string" + }, + "url_base": { + "nullable": false, + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "provider_id", + "provider_name", + "client_id", + "client_secret", + "url_base" + ] + } + }, + "additionalProperties": false, + "required": [ + "kind", + "data" + ] + } + ] + }, + "smtp": { + "nullable": true, + "type": "object", + "properties": { + "host": { + "nullable": false, + "type": "string" + }, + "port": { + "nullable": false, + "type": "integer", + "default": 587 + }, + "username": { + "nullable": false, + "type": "string", + "default": "synapse" + }, + "password": { + "nullable": false, + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "host", + "port", + "username", + "password" + ] + }, + "notifications": { + "nullable": false, + "type": "object", + "properties": { + "source_address": { + "nullable": false, + "type": "string", + "default": "synapse@example.org" + }, + "via_email": { + "nullable": false, + "type": "object", + "properties": { + "enabled_by_default": { + "nullable": false, + "type": "boolean", + "default": false + }, + "delay": { + "nullable": false, + "type": "string", + "default": "1h" + } + }, + "additionalProperties": false, + "required": [ + "enabled_by_default", + "delay" + ] + } + }, + "additionalProperties": false, + "required": [ + "source_address", + "via_email" + ] + }, + "admin_user": { + "nullable": false, + "type": "object", + "properties": { + "define": { + "nullable": false, + "type": "boolean", + "default": true + }, + "name": { + "nullable": false, + "type": "string", + "default": "admin" + }, + "password": { + "nullable": false, + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "define", + "name", + "password" + ] + } + }, + "additionalProperties": false, + "required": [ + "scheme", + "domain", + "database", + "element_url", + "title", + "federation", + "password_strict_policy", + "registration_shared_secret", + "authentication", + "smtp", + "notification", + "admin_user" + ] +} diff --git a/roles/synapse/defaults/main.json b/roles/synapse/defaults/main.json index 87eff2a..192c36b 100644 --- a/roles/synapse/defaults/main.json +++ b/roles/synapse/defaults/main.json @@ -1,34 +1,38 @@ { - "var_synapse_scheme": "https", - "var_synapse_domain": "synapse.example.org", - "var_synapse_database_kind": "sqlite", - "var_synapse_database_data_sqlite_path": "/var/synapse/data.sqlite", - "var_synapse_database_data_postgresql_host": "localhost", - "var_synapse_database_data_postgresql_port": 5432, - "var_synapse_database_data_postgresql_username": "synapse_user", - "var_synapse_database_data_postgresql_password": "REPLACE_ME", - "var_synapse_database_data_postgresql_schema": "synapse", - "var_synapse_element_url": "https://element.example.org", - "var_synapse_title": "Example | Matrix", - "var_synapse_federation_enable": true, - "var_synapse_federation_whitelist": [], - "var_synapse_password_strict_policy": true, - "var_synapse_registration_shared_secret": "REPLACE_ME", - "var_synapse_authentication_kind": "internal", - "var_synapse_authentication_data_authelia_provider_id": "authelia", - "var_synapse_authentication_data_authelia_provider_name": "Authelia", - "var_synapse_authentication_data_authelia_client_id": "synapse", - "var_synapse_authentication_data_authelia_client_secret": "REPLACE_ME", - "var_synapse_authentication_data_authelia_url_base": "https://authelia.example.org", - "var_synapse_smtp_host": "smtp.example.org", - "var_synapse_smtp_port": 587, - "var_synapse_smtp_username": "synapse@smtp.example.org", - "var_synapse_smtp_password": "REPLACE_ME", - "var_synapse_notifications_source_address": "synapse@example.org", - "var_synapse_notifications_via_email_enabled_by_default": false, - "var_synapse_notifications_via_email_delay": "1h", - "var_synapse_admin_user_define": true, - "var_synapse_admin_user_name": "admin", - "var_synapse_admin_user_password": "REPLACE_ME" + "cfg_synapse_defaults": { + "scheme": "https", + "database": { + "kind": "sqlite", + "data": { + "path": "/var/synapse/data.sqlite" + } + }, + "element_url": null, + "title": "Example | Matrix", + "federation": { + "enable": false, + "whitelist": [] + }, + "password_strict_policy": true, + "registration_shared_secret": null, + "authentication": { + "kind": "internal", + "data": {} + }, + "smtp": { + "port": 587, + "username": "synapse" + }, + "notifications": { + "source_address": "synapse@example.org", + "via_email": { + "enabled_by_default": false, + "delay": "1h" + } + }, + "admin_user": { + "define": true, + "name": "admin" + } + } } - diff --git a/roles/synapse/templates/homeserver.yaml.j2 b/roles/synapse/templates/homeserver.yaml.j2 index 55799bb..8c7399f 100644 --- a/roles/synapse/templates/homeserver.yaml.j2 +++ b/roles/synapse/templates/homeserver.yaml.j2 @@ -1,19 +1,19 @@ -{% if var_synapse_database_kind == 'sqlite' %} +{% if cfg_synapse.database.kind == 'sqlite' %} database: name: sqlite3 args: - database: {{var_synapse_database_sqlite_path}} + database: {{var.synapse.database.data.path}} {% endif %} -{% if var_synapse_database_kind == 'postgresql' %} +{% if cfg_synapse.database.kind == 'postgresql' %} database: name: psycopg2 args: - host: {{var_synapse_database_data_postgresql_host}} - port: {{var_synapse_database_data_postgresql_port | string}} - database: "{{var_synapse_database_data_postgresql_schema}}" - user: "{{var_synapse_database_data_postgresql_username}}" - password: "{{var_synapse_database_data_postgresql_password}}" + host: {{cfg_synapse.database.data.host}} + port: {{cfg_synapse.database.data.port | string}} + database: "{{cfg_synapse.database.data.schema}}" + user: "{{cfg_synapse.database.data.username}}" + password: "{{cfg_synapse.database.data.password}}" cp_min: 5 cp_max: 10 {% endif %} @@ -26,9 +26,11 @@ pid_file: "/var/run/matrix-synapse.pid" soft_file_limit: 0 -web_client_location: {{var_synapse_element_url}} +{% if cfg_synapse.element_url != None %} +web_client_location: {{cfg_synapse.element_url}} +{% endif %} -public_baseurl: {{var_synapse_scheme}}://{{var_synapse_domain}}/ +public_baseurl: {{var_synapse.scheme}}://{{var_synapse.domain}}/ listeners: - port: 8008 @@ -42,12 +44,12 @@ listeners: resources: - names: [client] compress: true -{% if var_synapse_federation_enable %} +{% if cfg_synapse.federation.enable %} - names: [federation] compress: false {% endif %} -federation_domain_whitelist: {{var_synapse_federation_whitelist | to_yaml}} +federation_domain_whitelist: {{cfg_synapse.federation.whitelist | to_yaml}} serve_server_wellknown: true @@ -89,8 +91,8 @@ max_spider_size: "10M" enable_registration_captcha: false recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify" -{% if var_synapse_registration_shared_secret != None %} -registration_shared_secret: "{{var_synapse_registration_shared_secret}}" +{% if cfg_synapse.registration.shared_secret != None %} +registration_shared_secret: "{{cfg_synapse.registration_shared_secret}}" {% endif %} oidc_config: @@ -99,23 +101,23 @@ oidc_config: # NOT an Ansible variable localpart_template: "{{"{{"}} user.preferred_username {{"}}"}}" -{% if var_synapse_authentication_kind == 'internal' %} +{% if var_synapse.authentication.kind == 'internal' %} enable_registration: true enable_registration_without_verification: true {% endif %} -{% if var_synapse_authentication_kind == 'authelia' %} +{% if var_synapse.authentication.kind == 'authelia' %} enable_registration: false enable_registration_without_verification: false oidc_providers: - - idp_id: "{{var_synapse_authentication_data_authelia_provider_id}}" - idp_name: "{{var_synapse_authentication_data_authelia_provider_name}}" + - idp_id: "{{cfg_synapse.authentication.data.provider_id}}" + idp_name: "{{cfg_synapse.authentication.data.provider_name}}" idp_icon: "mxc://authelia.com/cKlrTPsGvlpKxAYeHWJsdVHI" discover: true - issuer: "{{var_synapse_authentication_data_authelia_url_base}}" - client_id: "{{var_synapse_authentication_data_authelia_client_id}}" - client_secret: "{{var_synapse_authentication_data_authelia_client_secret}}" + issuer: "{{cfg_synapse.authentication.data.url_base}}" + client_id: "{{cfg_synapse.authentication.data.client_id}}" + client_secret: "{{cfg_synapse.authentication.data.client_secret}}" scopes: ["openid", "profile", "email"] allow_existing_users: true user_mapping_provider: @@ -166,19 +168,21 @@ password_config: policy: enabled: {{var_synapse_password_strict_policy | to_yaml}} +{% if cfg_synapse.smtp != None %} email: - smtp_host: "{{var_synapse_smtp_host}}" - smtp_port: {{var_synapse_smtp_port | to_yaml}} - smtp_user: "{{var_synapse_smtp_username}}" - smtp_pass: "{{var_synapse_smtp_password}}" + smtp_host: "{{cfg_synapse.smtp.host}}" + smtp_port: {{cfg_synapse.smtp.port | to_yaml}} + smtp_user: "{{cfg_synapse.smtp.username}}" + smtp_pass: "{{cfg_synapse.smtp.password}}" require_transport_security: true - notif_from: "%(app)s | {{var_synapse_title}} <{{var_synapse_notifications_source_address}}>" + notif_from: "%(app)s | {{var_synapse_title}} <{{cfg_synapse.notifications.source_address}}>" enable_notifs: true - notif_for_new_users: {{var_synapse_notifications_via_email_enabled_by_default | to_yaml}} - notif_delay_before_mail: {{var_synapse_notifications_via_email_delay}} + notif_for_new_users: {{cfg_synapse.notifications.via_email.enabled_by_default | to_yaml}} + notif_delay_before_mail: {{cfg_synapse.notifications.via_email.delay}} subjects: password_reset: "[%(server_name)s] Passwort zurücksetzen" email_validation: "[%(server_name)s] Nutzer-Konto-Freischaltung" +{% endif %} spam_checker: diff --git a/roles/synapse/vardef.json b/roles/synapse/vardef.json deleted file mode 100644 index 391dfdb..0000000 --- a/roles/synapse/vardef.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "scheme": { - "type": "string", - "mandatory": false - }, - "domain": { - "type": "string", - "mandatory": false - }, - "database_kind": { - "type": "string", - "mandatory": false, - "options": [ - "sqlite", - "postgresql" - ] - }, - "database_data_sqlite_path": { - "type": "string", - "mandatory": false - }, - "database_data_postgresql_host": { - "type": "string", - "mandatory": false - }, - "database_data_postgresql_port": { - "type": "integer", - "mandatory": false - }, - "database_data_postgresql_username": { - "type": "string", - "mandatory": false - }, - "database_data_postgresql_password": { - "type": "string", - "mandatory": true - }, - "database_data_postgresql_schema": { - "type": "string", - "mandatory": false - }, - "element_url": { - "type": "string", - "mandatory": false - }, - "title": { - "type": "string", - "mandatory": false - }, - "federation_enable": { - "type": "boolean", - "mandatory": false - }, - "federation_whitelist": { - "type": "array", - "items": { - "type": "string" - }, - "mandatory": false - }, - "password_strict_policy": { - "type": "boolean", - "mandatory": false - }, - "registration_shared_secret": { - "type": "string", - "mandatory": true - }, - "authentication_kind": { - "type": "string", - "mandatory": false, - "options": [ - "internal", - "authelia" - ] - }, - "authentication_data_authelia_provider_id": { - "type": "string", - "mandatory": false - }, - "authentication_data_authelia_provider_name": { - "type": "string", - "mandatory": false - }, - "authentication_data_authelia_client_id": { - "type": "string", - "mandatory": false - }, - "authentication_data_authelia_client_secret": { - "type": "string", - "mandatory": false - }, - "authentication_data_authelia_url_base": { - "type": "string", - "mandatory": false - }, - "smtp_host": { - "type": "string", - "mandatory": false - }, - "smtp_port": { - "type": "integer", - "mandatory": false - }, - "smtp_username": { - "type": "string", - "mandatory": false - }, - "smtp_password": { - "type": "string", - "mandatory": true - }, - "notifications_source_address": { - "type": "string", - "mandatory": false - }, - "notifications_via_email_enabled_by_default": { - "type": "boolean", - "mandatory": false - }, - "notifications_via_email_delay": { - "type": "string", - "mandatory": false - }, - "admin_user_define": { - "type": "boolean", - "mandatory": false - }, - "admin_user_name": { - "type": "string", - "mandatory": false - }, - "admin_user_password": { - "type": "string", - "mandatory": false - } -} diff --git a/tools/cfg-man b/tools/cfg-man index 6aa543b..14c56ff 100755 --- a/tools/cfg-man +++ b/tools/cfg-man @@ -127,6 +127,172 @@ def generate_overrides( ) +def investigate( + schema, + value +): + flaws = [] + if ("anyOf" in schema): + found = False + entries = [] + for sub_schema in schema["anyOf"]: + sub_flaws = investigate(sub_schema, value) + if (len(sub_flaws) <= 0): + found = True + break + else: + entries.append({"schema": sub_schema, "flaws": sub_flaws}) + if found: + pass + else: + flaws.append({"incident": "not_valid_against_any_sub_schema", "details": {"value": value, "results": entries}}) + else: + if (value is None): + if (not schema.get("nullable", False)): + flaws.append({"incident": "not_nullable", "details": {"value": str(type(value))}}) + else: + if (not ("type" in schema)): + raise ValueError("unhandled: %s" % _json.dumps(schema)) + else: + if (schema["type"] == "boolean"): + if (not (type(value) == bool)): + flaws.append({"incident": "wrong_type", "details": {"shall": str(bool), "is": str(type(value))}}) + else: + if (("enum" in schema) and not (value in schema["enum"])): + flaws.append({"incident": "not_in_enum", "details": {"enum": schema["enum"], "value": value}}) + elif (schema["type"] == "integer"): + if (not (type(value) == int)): + flaws.append({"incident": "wrong_type", "details": {"shall": str(int), "is": str(type(value))}}) + else: + if (("enum" in schema) and not (value in schema["enum"])): + flaws.append({"incident": "not_in_enum", "details": {"enum": schema["enum"], "value": value}}) + # todo: min,max,step,etc. + elif (schema["type"] == "number"): + if (not (type(value) == int) and not (type(value) == float)): + flaws.append({"incident": "wrong_type", "details": {"shall": str(float), "is": str(type(value))}}) + else: + if (("enum" in schema) and not (value in schema["enum"])): + flaws.append({"incident": "not_in_enum", "details": {"enum": schema["enum"], "value": value}}) + # todo: min,max,step,etc. + elif (schema["type"] == "string"): + if (not (type(value) == str)): + flaws.append({"incident": "wrong_type", "details": {"shall": str(str), "is": str(type(value))}}) + else: + if (("enum" in schema) and not (value in schema["enum"])): + flaws.append({"incident": "not_in_enum", "details": {"enum": schema["enum"], "value": value}}) + # todo: min,max,pattern,etc. + elif (schema["type"] == "object"): + if (not (type(value) == dict)): + flaws.append({"incident": "wrong_type", "details": {"shall": str(dict), "is": str(type(value))}}) + else: + entries = [] + for (property_key, property_value, ) in schema["properties"].items(): + if (not (property_key in value)): + if (not (property_key in schema.get("required", []))): + pass + else: + flaws.append({"incident": "mandatory_field_missing", "details": {"key": property_key}}) + else: + sub_flaws = investigate(property_value, value[property_key]) + if (len(sub_flaws) <= 0): + pass + else: + entries.append({"key": property_key, "result": sub_flaws}) + if (len(entries) <= 0): + pass + else: + flaws.append({"incident": "field_flaws", "details": entries}) + # todo: additionalProperties + # todo: required + else: + raise ValueError("unhandled: %s" % _json.dumps(schema)) + return flaws + + +def validate( + schema, + value +): + investigation = investigate(schema, value) + if False: + _sys.stderr.write( + _json.dumps( + investigation, + indent = "\t" + ) + + + "\n" + ) + return ( + len(investigation) + <= + 0 + ) + + +def reduce( + schema, + value +): + if False: + _sys.stderr.write( + _json.dumps( + { + "schema": schema, + "value": value, + }, + indent = "\t" + ) + + + "\n" + ) + if ("anyOf" in schema): + for sub_schema in schema["anyOf"]: + if validate(sub_schema, value): + return reduce(sub_schema, value) + else: + pass + raise ValueError("not valid against any sub schema") + else: + if (not ("default" in schema)): + return class_option_filled(value) + else: + if ( + (schema["type"] == "boolean") + or + (schema["type"] == "integer") + or + (schema["type"] == "number") + or + (schema["type"] == "string") + ): + if (value == schema["default"]): + return class_option_empty() + else: + return class_option_filled(value) + elif (schema["type"] == "array"): + if (value == schema["default"]): + return class_option_empty() + else: + return class_option_filled(value) + elif (schema["type"] == "object"): + if (not (type(value) == dict)): + raise ValueError("dict expected") + else: + value_out = {} + for (property_key, property_value, ) in schema["properties"].items(): + sub_result = reduce(property_value, value[property_key]) + if (not sub_result.is_filled()): + pass + else: + value_out[property_key] = sub_result.cull() + return class_option_filled(value_out) + # todo: additionalProperties + # toto: required + else: + raise ValueError("unhandled: %s" % _json.dumps(schema)) + + def role_name_derive( role_name ): @@ -143,6 +309,7 @@ def main( choices = [ "defaults", "overrides", + "reduce", ], metavar = "", ) @@ -172,6 +339,13 @@ def main( key = ("cfg_%s_overrides" % (role_name_derive(args.role))) result = {key: (raw.cull() if raw.is_filled() else {})} _sys.stdout.write(_json.dumps(result, indent = "\t") + "\n") + elif args.action == 'reduce': + cfg = _json.loads(_sys.stdin.read()) + result = reduce(cfg_schema, cfg) + if (not result.is_filled()): + print("?") + else: + _sys.stdout.write(_json.dumps(result.cull(), indent = "\t") + "\n") else: raise ValueError("invalid action: %s" % args.action)