mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-10-31 18:29:21 +00:00 
			
		
		
		
	Finished Mobilizon OIDC implementation
This commit is contained in:
		| @@ -1,15 +1,14 @@ | ||||
| # filter_plugins/docker_image.py | ||||
| def get_docker_image(applications, application_id, image_key:str=None): | ||||
|     image_key = image_key if image_key else application_id | ||||
|     docker = applications.get(application_id, {}).get("docker", {}) | ||||
|     version = docker.get("versions", {}).get(image_key) | ||||
|     image = docker.get("images", {}).get(image_key) | ||||
|  | ||||
| def get_docker_image(applications, application_id, image_key): | ||||
|     app = applications.get(application_id, {}) | ||||
|     docker = app.get("docker", {}) | ||||
|     images = docker.get("images", {}) | ||||
|     versions = docker.get("versions", {}) | ||||
|     version = versions.get(image_key) or app.get("version") | ||||
|     image = images.get(image_key) | ||||
|     if not image: | ||||
|         raise ValueError(f"Missing image for {application_id}:{image_key}") | ||||
|  | ||||
|     if not image or not version: | ||||
|         raise ValueError(f"Missing image or version for {application_id}:{image_key}") | ||||
|     if not version: | ||||
|         raise ValueError(f"Missing version for {application_id}:{image_key}") | ||||
|      | ||||
|     return f"{image}:{version}" | ||||
|  | ||||
|   | ||||
| @@ -9,10 +9,17 @@ | ||||
|  | ||||
| ## Helper Variables: | ||||
| _oidc_client_realm:         "{{ oidc.client.realm if oidc.client is defined and oidc.client.realm is defined else primary_domain }}" | ||||
| _oidc_client_issuer_url:    "{{ web_protocol }}://{{domains | get_domain('keycloak')}}/realms/{{_oidc_client_realm}}" | ||||
| _oidc_url:                  "{{  | ||||
|                                 (oidc.url  | ||||
|                                   if (oidc is defined and oidc.url is defined)  | ||||
|                                   else web_protocol ~ '://' ~ (domains | get_domain('keycloak')) | ||||
|                                 )  | ||||
|                             }}" | ||||
| _oidc_client_issuer_url:    "{{ _oidc_url }}/realms/{{_oidc_client_realm}}" | ||||
| _oidc_client_id:            "{{ oidc.client.id if oidc.client is defined and oidc.client.id is defined else primary_domain }}" | ||||
|  | ||||
| defaults_oidc: | ||||
|   url:                    "{{ _oidc_url }}" | ||||
|   client: | ||||
|     id:                   "{{ _oidc_client_id }}"                                            # Client identifier, typically matching your primary domain | ||||
| #   secret:                                                                                 # Client secret for authenticating with the OIDC provider (set in the inventory file). Recommend greater then 32 characters | ||||
|   | ||||
| @@ -41,7 +41,7 @@ FUNKWHALE_WEB_WORKERS=4 | ||||
| # your instance. It cannot be changed after initial deployment | ||||
| # without breaking your instance. | ||||
| FUNKWHALE_HOSTNAME={{domains | get_domain(application_id)}} | ||||
| FUNKWHALE_PROTOCOL=https | ||||
| FUNKWHALE_PROTOCOL={{ web_protocol }} | ||||
|  | ||||
| # Log level (debug, info, warning, error, critical) | ||||
| LOGLEVEL={% if enable_debug | bool %}debug{% else %}error{% endif %} | ||||
| @@ -60,7 +60,7 @@ LOGLEVEL={% if enable_debug | bool %}debug{% else %}error{% endif %} | ||||
| # (returns `noreply%40youremail.host`) | ||||
| # EMAIL_CONFIG=smtp://user:password@youremail.host:25 | ||||
| # EMAIL_CONFIG=smtp+ssl://user:password@youremail.host:465 | ||||
| EMAIL_CONFIG=smtp+tls://no-reply:{{ users['no-reply'].mailu_token }}@{{system_email.host}}:{{system_email.port}} | ||||
| EMAIL_CONFIG=smtp+tls://{{ users['no-reply'].username }}:{{ users['no-reply'].mailu_token }}@{{system_email.host}}:{{system_email.port}} | ||||
|  | ||||
| # Make e-mail verification mandatory before using the service | ||||
| # Doesn't apply to admins. | ||||
|   | ||||
| @@ -1,2 +0,0 @@ | ||||
| # Todo | ||||
| - Implement | ||||
| @@ -10,4 +10,10 @@ | ||||
|     domain:   "{{ domains | get_domain(application_id) }}" | ||||
|     http_port:   "{{ ports.localhost.http[application_id] }}" | ||||
|  | ||||
| - name: add config.exs | ||||
|   template: | ||||
|     src:  "config.exs.j2"  | ||||
|     dest: "{{ mobilizon_host_conf_exs_file }}" | ||||
|   notify: docker compose up | ||||
|  | ||||
| - include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml" | ||||
|   | ||||
							
								
								
									
										278
									
								
								roles/docker-mobilizon/templates/config.exs.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								roles/docker-mobilizon/templates/config.exs.j2
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,278 @@ | ||||
| # Mobilizon instance configuration | ||||
|  | ||||
| import Config | ||||
| import Mobilizon.Service.Config.Helpers | ||||
|  | ||||
| {:ok, _} = Application.ensure_all_started(:tls_certificate_check) | ||||
|  | ||||
| loglevels = [ | ||||
|   :emergency, | ||||
|   :alert, | ||||
|   :critical, | ||||
|   :error, | ||||
|   :warning, | ||||
|   :notice, | ||||
|   :info, | ||||
|   :debug | ||||
| ] | ||||
|  | ||||
| loglevel_env = System.get_env("MOBILIZON_LOGLEVEL", "error") | ||||
|  | ||||
| loglevel = | ||||
|   if loglevel_env in Enum.map(loglevels, &to_string/1) do | ||||
|     String.to_existing_atom(loglevel_env) | ||||
|   else | ||||
|     :error | ||||
|   end | ||||
|  | ||||
| listen_ip = System.get_env("MOBILIZON_INSTANCE_LISTEN_IP", "0.0.0.0") | ||||
|  | ||||
| listen_ip = | ||||
|   case listen_ip |> to_charlist() |> :inet.parse_address() do | ||||
|     {:ok, listen_ip} -> listen_ip | ||||
|     _ -> raise "MOBILIZON_INSTANCE_LISTEN_IP does not match the expected IP format." | ||||
|   end | ||||
|  | ||||
| config :mobilizon, Mobilizon.Web.Endpoint, | ||||
|   server: true, | ||||
|   url: [host: System.get_env("MOBILIZON_INSTANCE_HOST", "mobilizon.lan")], | ||||
|   http: [ | ||||
|     port: String.to_integer(System.get_env("MOBILIZON_INSTANCE_PORT", "4000")), | ||||
|     ip: listen_ip | ||||
|   ], | ||||
|   secret_key_base: System.get_env("MOBILIZON_INSTANCE_SECRET_KEY_BASE", "changethis") | ||||
|  | ||||
| config :mobilizon, Mobilizon.Web.Auth.Guardian, | ||||
|   secret_key: System.get_env("MOBILIZON_INSTANCE_SECRET_KEY", "changethis") | ||||
|  | ||||
| config :mobilizon, :instance, | ||||
|   name: System.get_env("MOBILIZON_INSTANCE_NAME", "Mobilizon"), | ||||
|   description: "Change this to a proper description of your instance", | ||||
|   hostname: System.get_env("MOBILIZON_INSTANCE_HOST", "mobilizon.lan"), | ||||
|   registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN", "false") == "true", | ||||
|   registration_email_allowlist: | ||||
|     System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_EMAIL_ALLOWLIST", "") | ||||
|     |> String.split(",", trim: true), | ||||
|   registration_email_denylist: | ||||
|     System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_EMAIL_DENYLIST", "") | ||||
|     |> String.split(",", trim: true), | ||||
|   disable_database_login: | ||||
|     System.get_env("MOBILIZON_INSTANCE_DISABLE_DATABASE_LOGIN", "false") == "true", | ||||
|   default_language: System.get_env("MOBILIZON_INSTANCE_DEFAULT_LANGUAGE", "en"), | ||||
|   demo: System.get_env("MOBILIZON_INSTANCE_DEMO", "false") == "true", | ||||
|   allow_relay: System.get_env("MOBILIZON_INSTANCE_ALLOW_RELAY", "true") == "true", | ||||
|   federating: System.get_env("MOBILIZON_INSTANCE_FEDERATING", "true") == "true", | ||||
|   enable_instance_feeds: | ||||
|     System.get_env("MOBILIZON_INSTANCE_ENABLE_INSTANCE_FEEDS", "true") == "true", | ||||
|   email_from: System.get_env("MOBILIZON_INSTANCE_EMAIL", "noreply@mobilizon.lan"), | ||||
|   email_reply_to: System.get_env("MOBILIZON_REPLY_EMAIL", "noreply@mobilizon.lan") | ||||
|  | ||||
| config :mobilizon, Mobilizon.Storage.Repo, | ||||
|   adapter: Ecto.Adapters.Postgres, | ||||
|   username: System.get_env("MOBILIZON_DATABASE_USERNAME", "username"), | ||||
|   password: System.get_env("MOBILIZON_DATABASE_PASSWORD", "password"), | ||||
|   database: System.get_env("MOBILIZON_DATABASE_DBNAME", "mobilizon"), | ||||
|   hostname: System.get_env("MOBILIZON_DATABASE_HOST", "postgres"), | ||||
|   port: System.get_env("MOBILIZON_DATABASE_PORT", "5432"), | ||||
|   ssl: System.get_env("MOBILIZON_DATABASE_SSL", "false") == "true", | ||||
|   pool_size: 10 | ||||
|  | ||||
| config :logger, level: loglevel | ||||
|  | ||||
| config :mobilizon, Mobilizon.Web.Email.Mailer, | ||||
|   adapter: Swoosh.Adapters.SMTP, | ||||
|   relay: System.get_env("MOBILIZON_SMTP_SERVER", "localhost"), | ||||
|   port: System.get_env("MOBILIZON_SMTP_PORT", "25"), | ||||
|   username: System.get_env("MOBILIZON_SMTP_USERNAME", nil), | ||||
|   password: System.get_env("MOBILIZON_SMTP_PASSWORD", nil), | ||||
|   tls: System.get_env("MOBILIZON_SMTP_TLS", "if_available"), | ||||
|   tls_options: | ||||
|     :tls_certificate_check.options(System.get_env("MOBILIZON_SMTP_SERVER", "localhost")), | ||||
|   ssl: System.get_env("MOBILIZON_SMTP_SSL", "false"), | ||||
|   retries: 1, | ||||
|   no_mx_lookups: false, | ||||
|   auth: System.get_env("MOBILIZON_SMTP_AUTH", "if_available") | ||||
|  | ||||
| config :geolix, | ||||
|   databases: [ | ||||
|     %{ | ||||
|       id: :city, | ||||
|       adapter: Geolix.Adapter.MMDB2, | ||||
|       source: "/var/lib/mobilizon/geo_db/GeoLite2-City.mmdb" | ||||
|     } | ||||
|   ] | ||||
|  | ||||
| config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, | ||||
|   uploads: System.get_env("MOBILIZON_UPLOADS", "/var/lib/mobilizon/uploads") | ||||
|  | ||||
| formats = | ||||
|   if System.get_env("MOBILIZON_EXPORTS_FORMAT_CSV_ENABLED", "true") == "true" do | ||||
|     [Mobilizon.Service.Export.Participants.CSV] | ||||
|   else | ||||
|     [] | ||||
|   end | ||||
|  | ||||
| formats = | ||||
|   if System.get_env("MOBILIZON_EXPORTS_FORMAT_PDF_ENABLED", "true") == "true" do | ||||
|     formats ++ [Mobilizon.Service.Export.Participants.PDF] | ||||
|   else | ||||
|     formats | ||||
|   end | ||||
|  | ||||
| formats = | ||||
|   if System.get_env("MOBILIZON_EXPORTS_FORMAT_ODS_ENABLED", "true") == "true" do | ||||
|     formats ++ [Mobilizon.Service.Export.Participants.ODS] | ||||
|   else | ||||
|     formats | ||||
|   end | ||||
|  | ||||
| config :mobilizon, :exports, | ||||
|   path: System.get_env("MOBILIZON_UPLOADS_EXPORTS", "/var/lib/mobilizon/uploads/exports"), | ||||
|   formats: formats | ||||
|  | ||||
| config :tz_world, | ||||
|   data_dir: System.get_env("MOBILIZON_TIMEZONES_DIR", "/var/lib/mobilizon/timezones") | ||||
|  | ||||
| config :tzdata, :data_dir, System.get_env("MOBILIZON_TZDATA_DIR", "/var/lib/mobilizon/tzdata") | ||||
|  | ||||
| config :web_push_encryption, :vapid_details, | ||||
|   subject: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_SUBJECT", nil), | ||||
|   public_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PUBLIC_KEY", nil), | ||||
|   private_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PRIVATE_KEY", nil) | ||||
|  | ||||
| geospatial_service = | ||||
|   case System.get_env("MOBILIZON_GEOSPATIAL_SERVICE", "Nominatim") do | ||||
|     "Nominatim" -> Mobilizon.Service.Geospatial.Nominatim | ||||
|     "Addok" -> Mobilizon.Service.Geospatial.Addok | ||||
|     "Photon" -> Mobilizon.Service.Geospatial.Photon | ||||
|     "GoogleMaps" -> Mobilizon.Service.Geospatial.GoogleMaps | ||||
|     "MapQuest" -> Mobilizon.Service.Geospatial.MapQuest | ||||
|     "Mimirsbrunn" -> Mobilizon.Service.Geospatial.Mimirsbrunn | ||||
|     "Pelias" -> Mobilizon.Service.Geospatial.Pelias | ||||
|     "Hat" -> Mobilizon.Service.Geospatial.Hat | ||||
|   end | ||||
|  | ||||
| config :mobilizon, Mobilizon.Service.Geospatial, service: geospatial_service | ||||
|  | ||||
| config :mobilizon, Mobilizon.Service.Geospatial.Nominatim, | ||||
|   endpoint: | ||||
|     System.get_env( | ||||
|       "MOBILIZON_GEOSPATIAL_NOMINATIM_ENDPOINT", | ||||
|       "https://nominatim.openstreetmap.org" | ||||
|     ), | ||||
|   api_key: System.get_env("MOBILIZON_GEOSPATIAL_NOMINATIM_API_KEY", nil) | ||||
|  | ||||
| config :mobilizon, Mobilizon.Service.Geospatial.Addok, | ||||
|   endpoint: | ||||
|     System.get_env("MOBILIZON_GEOSPATIAL_ADDOK_ENDPOINT", "https://api-adresse.data.gouv.fr") | ||||
|  | ||||
| config :mobilizon, Mobilizon.Service.Geospatial.Photon, | ||||
|   endpoint: System.get_env("MOBILIZON_GEOSPATIAL_PHOTON_ENDPOINT", "https://photon.komoot.de") | ||||
|  | ||||
| config :mobilizon, Mobilizon.Service.Geospatial.GoogleMaps, | ||||
|   api_key: System.get_env("MOBILIZON_GEOSPATIAL_GOOGLE_MAPS_API_KEY", nil), | ||||
|   fetch_place_details: true | ||||
|  | ||||
| config :mobilizon, Mobilizon.Service.Geospatial.MapQuest, | ||||
|   api_key: System.get_env("MOBILIZON_GEOSPATIAL_MAP_QUEST_API_KEY", nil) | ||||
|  | ||||
| config :mobilizon, Mobilizon.Service.Geospatial.Mimirsbrunn, | ||||
|   endpoint: System.get_env("MOBILIZON_GEOSPATIAL_MIMIRSBRUNN_ENDPOINT", nil) | ||||
|  | ||||
| config :mobilizon, Mobilizon.Service.Geospatial.Pelias, | ||||
|   endpoint: System.get_env("MOBILIZON_GEOSPATIAL_PELIAS_ENDPOINT", nil) | ||||
|  | ||||
| sentry_dsn = System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_DSN", nil) | ||||
|  | ||||
| included_environments = if sentry_dsn, do: ["prod"], else: [] | ||||
|  | ||||
| config :sentry, | ||||
|   dsn: sentry_dsn, | ||||
|   included_environments: included_environments, | ||||
|   release: to_string(Application.spec(:mobilizon, :vsn)) | ||||
|  | ||||
| config :logger, Sentry.LoggerBackend, | ||||
|   capture_log_messages: true, | ||||
|   level: :error | ||||
|  | ||||
| if sentry_dsn != nil do | ||||
|   config :mobilizon, Mobilizon.Service.ErrorReporting, | ||||
|     adapter: Mobilizon.Service.ErrorReporting.Sentry | ||||
| end | ||||
|  | ||||
| matomo_enabled = System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_ENABLED", "false") == "true" | ||||
| matomo_endpoint = System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_ENDPOINT", nil) | ||||
| matomo_site_id = System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_SITE_ID", nil) | ||||
|  | ||||
| matomo_tracker_file_name = | ||||
|   System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_TRACKER_FILE_NAME", "matomo") | ||||
|  | ||||
| matomo_host = host_from_uri(matomo_endpoint) | ||||
|  | ||||
| analytics_providers = | ||||
|   if matomo_enabled do | ||||
|     [Mobilizon.Service.FrontEndAnalytics.Matomo] | ||||
|   else | ||||
|     [] | ||||
|   end | ||||
|  | ||||
| analytics_providers = | ||||
|   if sentry_dsn != nil do | ||||
|     analytics_providers ++ [Mobilizon.Service.FrontEndAnalytics.Sentry] | ||||
|   else | ||||
|     analytics_providers | ||||
|   end | ||||
|  | ||||
| config :mobilizon, :analytics, providers: analytics_providers | ||||
|  | ||||
| matomo_csp = | ||||
|   if matomo_enabled and matomo_host do | ||||
|     [ | ||||
|       connect_src: [matomo_host], | ||||
|       script_src: [matomo_host], | ||||
|       img_src: [matomo_host] | ||||
|     ] | ||||
|   else | ||||
|     [] | ||||
|   end | ||||
|  | ||||
| config :mobilizon, Mobilizon.Service.FrontEndAnalytics.Matomo, | ||||
|   enabled: matomo_enabled, | ||||
|   host: matomo_endpoint, | ||||
|   siteId: matomo_site_id, | ||||
|   trackerFileName: matomo_tracker_file_name, | ||||
|   csp: matomo_csp | ||||
|  | ||||
| config :mobilizon, Mobilizon.Service.FrontEndAnalytics.Sentry, | ||||
|   enabled: sentry_dsn != nil, | ||||
|   dsn: sentry_dsn, | ||||
|   tracesSampleRate: 1.0, | ||||
|   organization: System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_ORGANISATION", nil), | ||||
|   project: System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_PROJECT", nil), | ||||
|   host: System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_HOST", nil), | ||||
|   csp: [ | ||||
|     connect_src: | ||||
|       System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_HOST", "") |> String.split(" ", trim: true) | ||||
|   ] | ||||
|  | ||||
| {% if applications | is_feature_enabled('oidc',application_id) %} | ||||
| config :ueberauth, | ||||
|        Ueberauth, | ||||
|        providers: [ | ||||
|          keycloak: {Ueberauth.Strategy.Keycloak, [default_scope: "openid profile email"]} | ||||
|        ] | ||||
|  | ||||
| config :mobilizon, :auth, | ||||
|   oauth_consumer_strategies: [ | ||||
|     {:keycloak, "{{ oidc.button_text }}"} | ||||
|   ] | ||||
|  | ||||
| config :ueberauth, Ueberauth.Strategy.Keycloak.OAuth, | ||||
|   client_id: "{{ oidc.client.id }}", | ||||
|   client_secret: "{{ oidc.client.secret }}", | ||||
|   site: "{{ oidc.url }}", | ||||
|   authorize_url: "{{ oidc.client.authorize_url }}", | ||||
|   token_url: "{{ oidc.client.token_url }}", | ||||
|   userinfo_url: "{{ oidc.client.user_info_url }}", | ||||
|   token_method: :post | ||||
| {% endif %} | ||||
| @@ -1,21 +1,19 @@ | ||||
| version: "3" | ||||
|  | ||||
| services: | ||||
|  | ||||
| {% include 'roles/docker-central-database/templates/services/' + database_type + '.yml.j2' %} | ||||
|  | ||||
|   mobilizon: | ||||
|   application: | ||||
|     image: "{{ applications[application_id].images[application_id] }}" | ||||
|     volumes: | ||||
|       - uploads:/var/lib/mobilizon/uploads | ||||
|       # - ./config.exs:/etc/mobilizon/config.exs:ro | ||||
|       - {{ mobilizon_host_conf_exs_file }}:/etc/mobilizon/config.exs:ro | ||||
|     ports: | ||||
|       - "127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ mobilizon_exposed_docker_port }}" | ||||
|   healthcheck: | ||||
|     test: ["CMD", "curl", "-f", "http://127.0.0.1:{{ mobilizon_exposed_docker_port }}"] | ||||
|     interval: 30s | ||||
|     timeout: 10s | ||||
|     retries: 3 | ||||
|     healthcheck: | ||||
|       test: ["CMD", "curl", "-f", "http://127.0.0.1:{{ mobilizon_exposed_docker_port }}"] | ||||
|       interval: 30s | ||||
|       timeout: 10s | ||||
|       retries: 3 | ||||
| {% include 'roles/docker-compose/templates/services/base.yml.j2' %} | ||||
| {% include 'templates/docker/container/depends-on-just-database.yml.j2' %} | ||||
| {% include 'templates/docker/container/networks.yml.j2' %} | ||||
|   | ||||
| @@ -67,12 +67,12 @@ MOBILIZON_DATABASE_PORT={{ database_port }} | ||||
| # A secret key used as a base to generate secrets for encrypting and signing data. | ||||
| # Make sure it's long enough (~64 characters should be fine) | ||||
| # You can run `openssl rand -base64 48` to generate such a secret | ||||
| MOBILIZON_INSTANCE_SECRET_KEY_BASE={{ applications[application_id].secret_key_base }} | ||||
| MOBILIZON_INSTANCE_SECRET_KEY_BASE={{ applications[application_id].credentials.secret_key_base }} | ||||
|  | ||||
| # A secret key used as a base to generate JWT tokens | ||||
| # Make sure it's long enough (~64 characters should be fine) | ||||
| # You can run `openssl rand -base64 48` to generate such a secret | ||||
| MOBILIZON_INSTANCE_SECRET_KEY={{ applications[application_id].secret_key }} | ||||
| MOBILIZON_INSTANCE_SECRET_KEY={{ applications[application_id].credentials.secret_key }} | ||||
|  | ||||
|  | ||||
| ###################################################### | ||||
| @@ -95,35 +95,4 @@ MOBILIZON_SMTP_SSL=false | ||||
| # Allowed values: always (TLS), never (Clear) and if_available (STARTTLS) | ||||
| # Make sure to match the port value as well | ||||
| # Defaults to "if_available" | ||||
| MOBILIZON_SMTP_TLS={% if system_email.tls %}TLS{% elif system_email.start_tls %}STARTTLS{% else %}Clear{% endif %} | ||||
|  | ||||
| {% if applications | is_feature_enabled('oidc',application_id) %} | ||||
| #################################### | ||||
| # ▶️ Mobilizon OIDC Configuration | ||||
| #################################### | ||||
|  | ||||
| AUTHENTICATION_STRATEGIES=open_id_connect | ||||
|  | ||||
| # Display name of the OIDC login button | ||||
| UEBERAUTH_OPENID_CONNECT_DISPLAY_NAME="{{ oidc.button_text }}" | ||||
|  | ||||
| # Use discovery to automatically fetch OIDC provider settings | ||||
| UEBERAUTH_OPENID_CONNECT_DISCOVERY_DOCUMENT={{ oidc.client.discovery_document }} | ||||
|  | ||||
| # OIDC OAuth2 client credentials | ||||
| UEBERAUTH_OPENID_CONNECT_CLIENT_ID={{ oidc.client.id }} | ||||
| UEBERAUTH_OPENID_CONNECT_CLIENT_SECRET={{ oidc.client.secret }} | ||||
|  | ||||
| # Redirect URI for the OIDC callback | ||||
| UEBERAUTH_OPENID_CONNECT_REDIRECT_URI={{ mobilizon_oidc_callback_url }} | ||||
|  | ||||
| # Scope and response type for OIDC | ||||
| UEBERAUTH_OPENID_CONNECT_SCOPE=openid email profile | ||||
| UEBERAUTH_OPENID_CONNECT_RESPONSE_TYPE=code | ||||
|  | ||||
| # Claim/field used to uniquely identify the user | ||||
| UEBERAUTH_OPENID_CONNECT_UID_FIELD={{ oidc.attributes.username }} | ||||
|  | ||||
| # Optional email verification behavior | ||||
| UEBERAUTH_OPENID_CONNECT_ASSUME_EMAIL_IS_VERIFIED=true | ||||
| {% endif %} | ||||
| MOBILIZON_SMTP_TLS={% if system_email.tls %}TLS{% elif system_email.start_tls %}STARTTLS{% else %}Clear{% endif %} | ||||
| @@ -3,4 +3,15 @@ images: | ||||
|   mobilizon:  "docker.io/framasoft/mobilizon" | ||||
| features: | ||||
|   central_database: true | ||||
|   oidc:             true | ||||
|   oidc:             true | ||||
| csp: | ||||
|   flags: | ||||
|     script-src-elem: | ||||
|       unsafe-inline:  true | ||||
|     script-src: | ||||
|       unsafe-eval:  true | ||||
| domains: | ||||
|   canonical: | ||||
|     - "event.{{ primary_domain }}" | ||||
|   aliases: | ||||
|     - "events.{{ primary_domain }}" | ||||
| @@ -1,4 +1,8 @@ | ||||
| application_id:                 mobilizon | ||||
| database_type:                  "mariadb" | ||||
|  | ||||
| database_type:                  "postgres" | ||||
| database_gis_enabled:           true | ||||
|  | ||||
| mobilizon_oidc_callback_url:    "{{ web_protocol }}://{{ domains | get_domain(application_id) }}/auth/openid_connect/callback" | ||||
| mobilizon_exposed_docker_port:  4000 | ||||
| mobilizon_exposed_docker_port:  4000 | ||||
| mobilizon_host_conf_exs_file:   "{{docker_compose.directories.config}}config.exs" | ||||
| @@ -9,7 +9,7 @@ | ||||
| - name: Install PostgreSQL | ||||
|   docker_container: | ||||
|     name: "{{ applications.postgres.hostname }}" | ||||
|     image: "postgres:{{applications.postgres.version}}" | ||||
|     image: "{{ applications | get_docker_image(application_id) }}" | ||||
|     detach: yes | ||||
|     env: | ||||
|       POSTGRES_PASSWORD: "{{ applications.postgres.credentials.postgres_password }}" | ||||
| @@ -118,6 +118,21 @@ | ||||
|       GRANT CREATE ON SCHEMA public TO {{ database_username }}; | ||||
|       ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO {{ database_username }}; | ||||
|  | ||||
| - name: Ensure PostGIS-related extensions are installed | ||||
|   community.postgresql.postgresql_ext: | ||||
|     db: "{{ database_name }}" | ||||
|     ext: "{{ item }}" | ||||
|     state: present | ||||
|     login_user: postgres | ||||
|     login_password: "{{ applications.postgres.credentials.postgres_password }}" | ||||
|     login_host: 127.0.0.1 | ||||
|     login_port: "{{ database_port }}" | ||||
|   loop: | ||||
|     - postgis | ||||
|     - pg_trgm | ||||
|     - unaccent | ||||
|   when: database_gis_enabled is defined and database_gis_enabled | ||||
|  | ||||
| - name: Run the docker_postgres tasks once | ||||
|   set_fact: | ||||
|     run_once_docker_postgres: true | ||||
|   | ||||
| @@ -1,3 +1,10 @@ | ||||
| # Please set an version in your inventory file - Rolling release for postgres isn't recommended | ||||
| version:  "latest" | ||||
| hostname: "central-postgres" | ||||
| hostname: "central-postgres" | ||||
| docker: | ||||
|   images: | ||||
|     # Postgis is necessary for mobilizon | ||||
|     postgres: postgis/postgis | ||||
|   versions:   | ||||
|     # Please set an version in your inventory file!  | ||||
|     # Rolling release isn't recommended | ||||
|     postgres: "latest" | ||||
|  | ||||
|   | ||||
| @@ -30,11 +30,15 @@ class TestDeprecatedVersionKey(unittest.TestCase): | ||||
|             uses_version = 'version' in config | ||||
|             uses_images = 'images' in config | ||||
|  | ||||
|             if uses_version and not uses_images: | ||||
|             if uses_version: | ||||
|                 warnings.append( | ||||
|                     f"[DEPRECATION WARNING] {role_path.name}/vars/configuration.yml: " | ||||
|                     f"'version:' is set, but 'images:' is missing. " | ||||
|                     f"'version' is deprecated and must only be set if 'images' is present." | ||||
|                     f"'version' is deprecated. Replace it by docker.versions[version]." | ||||
|                 ) | ||||
|             if uses_images: | ||||
|                 warnings.append( | ||||
|                     f"[DEPRECATION WARNING] {role_path.name}/vars/configuration.yml: " | ||||
|                     f"'images' is deprecated. Replace it by docker.images[image]." | ||||
|                 ) | ||||
|  | ||||
|         if warnings: | ||||
|   | ||||
| @@ -33,55 +33,15 @@ class TestDockerRoleImagesConfiguration(unittest.TestCase): | ||||
|                 errors.append(f"{role_path.name}: YAML parse error: {e}") | ||||
|                 continue | ||||
|  | ||||
|             images = config.get("images") | ||||
|             images = config.get("docker",{}).get("images") | ||||
|             if not images: | ||||
|                 warnings.append(f"[WARNING] {role_path.name}: No 'images' key in configuration.yml") | ||||
|                 warnings.append(f"[WARNING] {role_path.name}: No 'docker.images' key in configuration.yml") | ||||
|                 continue | ||||
|  | ||||
|             if not isinstance(images, dict): | ||||
|                 errors.append(f"{role_path.name}: 'images' must be a dict in configuration.yml") | ||||
|                 continue | ||||
|  | ||||
|             for key, value in images.items(): | ||||
|                 if not key or not value or not isinstance(key, str) or not isinstance(value, str): | ||||
|                     errors.append(f"{role_path.name}: images['{key}'] is invalid (must be non-empty string key and value)") | ||||
|                     continue | ||||
|  | ||||
|                 # Improved regex: matches both ' and " and allows whitespace | ||||
|                 pattern = ( | ||||
|                     r'image:\s*["\']\{\{\s*applications\[application_id\]\.images\.' + re.escape(key) + r'\s*\}\}["\']' | ||||
|                 ) | ||||
|  | ||||
|                 # innerhalb Deines Loops | ||||
|                 pattern2 = ( | ||||
|                     r'image:\s*["\']\{\{\s*'                          # image: "{{ | ||||
|                     r'applications\[\s*application_id\s*\]\.images'   # applications[ application_id ].images | ||||
|                     r'\[\s*application_id\s*\]\s*'                    # [ application_id ] | ||||
|                     r'\}\}["\']'                                      # }}" oder }}" | ||||
|                 ) | ||||
|  | ||||
|  | ||||
|                 for tmpl_file in [ | ||||
|                     role_path / "templates" / "docker-compose.yml.j2", | ||||
|                     role_path / "templates" / "env.j2", | ||||
|                 ]: | ||||
|                     if not tmpl_file.exists(): | ||||
|                         continue | ||||
|                     content = tmpl_file.read_text("utf-8") | ||||
|                     if re.search(pattern, content): | ||||
|                         break | ||||
|                     if key == main.get('application_id') and re.search(pattern2, content): | ||||
|                         break | ||||
|                 else: | ||||
|                     # Dieser Block wird nur ausgeführt, wenn kein `break` ausgelöst wurde | ||||
|                     errors.append( | ||||
|                         f"{role_path.name}: image key '{key}' is not referenced as " | ||||
|                         f"image: \"{{{{ applications[application_id].images.{key} }}}}\" or " | ||||
|                         f"\"{{{{ applications[application_id].images[application_id] }}}}\" " | ||||
|                         "in docker-compose.yml.j2 or env.j2" | ||||
|                     ) | ||||
|  | ||||
|  | ||||
|                 # OPTIONAL: Check if the image is available locally via docker images | ||||
|                 # from shutil import which | ||||
|                 # import subprocess | ||||
|   | ||||
| @@ -18,27 +18,14 @@ class TestGetDockerImage(unittest.TestCase): | ||||
|             "akaunting": { | ||||
|                 "version": "1.0.0", | ||||
|                 "docker": { | ||||
|                     "images": { "akaunting": "docker.io/akaunting/akaunting" }, | ||||
|                     "versions": { "akaunting": "2.0.0" } | ||||
|                     "images": {"akaunting": "docker.io/akaunting/akaunting"}, | ||||
|                     "versions": {"akaunting": "2.0.0"} | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         result = self.get_docker_image(applications, "akaunting", "akaunting") | ||||
|         self.assertEqual(result, "docker.io/akaunting/akaunting:2.0.0") | ||||
|  | ||||
|     def test_fallback_to_application_version(self): | ||||
|         applications = { | ||||
|             "akaunting": { | ||||
|                 "version": "1.2.3", | ||||
|                 "docker": { | ||||
|                     "images": { "akaunting": "ghcr.io/akaunting/akaunting" }, | ||||
|                     "versions": {} | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         result = self.get_docker_image(applications, "akaunting", "akaunting") | ||||
|         self.assertEqual(result, "ghcr.io/akaunting/akaunting:1.2.3") | ||||
|  | ||||
|     def test_missing_image_raises_error(self): | ||||
|         applications = { | ||||
|             "akaunting": { | ||||
| @@ -56,7 +43,7 @@ class TestGetDockerImage(unittest.TestCase): | ||||
|         applications = { | ||||
|             "akaunting": { | ||||
|                 "docker": { | ||||
|                     "images": { "akaunting": "some/image" }, | ||||
|                     "images": {"akaunting": "some/image"}, | ||||
|                     "versions": {} | ||||
|                 } | ||||
|             } | ||||
| @@ -64,5 +51,45 @@ class TestGetDockerImage(unittest.TestCase): | ||||
|         with self.assertRaises(ValueError): | ||||
|             self.get_docker_image(applications, "akaunting", "akaunting") | ||||
|  | ||||
|     # --- new: Default image_key uses application_id if none provided --- | ||||
|     def test_default_image_key_uses_application_id(self): | ||||
|         applications = { | ||||
|             "myapp": { | ||||
|                 "version": "3.0.0", | ||||
|                 "docker": { | ||||
|                     "images": {"myapp": "registry/myapp"}, | ||||
|                     "versions": {"myapp": "4.5.6"} | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         # No image_key argument → falls back to application_id | ||||
|         result = self.get_docker_image(applications, "myapp") | ||||
|         self.assertEqual(result, "registry/myapp:4.5.6") | ||||
|  | ||||
|     # --- new: Alternate image_key lookup --- | ||||
|     def test_alternate_image_key(self): | ||||
|         applications = { | ||||
|             "service": { | ||||
|                 "version": "9.9.9", | ||||
|                 "docker": { | ||||
|                     "images": { | ||||
|                         "service": "registry/service", | ||||
|                         "db":      "registry/service-db" | ||||
|                     }, | ||||
|                     "versions": { | ||||
|                         "db": "2.2.2" | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         result = self.get_docker_image(applications, "service", "db") | ||||
|         self.assertEqual(result, "registry/service-db:2.2.2") | ||||
|  | ||||
|     # --- new: Missing application raises error --- | ||||
|     def test_missing_application_raises_error(self): | ||||
|         applications = {} | ||||
|         with self.assertRaises(ValueError): | ||||
|             self.get_docker_image(applications, "does_not_exist") | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     unittest.main() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user