From f5421b17f034fe57141bf80f145e2c029f2751e4 Mon Sep 17 00:00:00 2001 From: Daniel Berteaud Date: Mon, 22 Jul 2024 10:00:12 +0200 Subject: [PATCH] Update to 2024-07-22 10:00 --- roles/jitsi/defaults/main.yml | 137 ++--- roles/jitsi/tasks/facts.yml | 17 +- roles/jitsi/templates/jicofo/jicofo.conf.j2 | 14 +- roles/jitsi/templates/prosody.cfg.lua.j2 | 73 ++- roles/prosody/defaults/main.yml | 18 + roles/prosody/files/prosody-ansible.te | 11 + roles/prosody/files/token/util.lib.lua | 539 ++++++++++++++++++++ roles/prosody/tasks/install.yml | 15 +- roles/prosody/tasks/main.yml | 4 + roles/prosody/tasks/selinux.yml | 28 + 10 files changed, 770 insertions(+), 86 deletions(-) create mode 100644 roles/prosody/files/prosody-ansible.te create mode 100644 roles/prosody/files/token/util.lib.lua create mode 100644 roles/prosody/tasks/selinux.yml diff --git a/roles/jitsi/defaults/main.yml b/roles/jitsi/defaults/main.yml index a358ed6..d875acd 100644 --- a/roles/jitsi/defaults/main.yml +++ b/roles/jitsi/defaults/main.yml @@ -14,7 +14,7 @@ jitsi_jigasi_git_url: https://github.com/jitsi/jigasi.git jitsi_meet_git_url: https://github.com/jitsi/jitsi-meet.git # Should ansible handle upgrades, or only initial install ? -jitsi_manage_upgrade: True +jitsi_manage_upgrade: true # XMPP server to connect to. Default is the same machine jitsi_xmpp_server: "{{ inventory_hostname }}" @@ -33,14 +33,26 @@ jitsi_stun_servers: [] jitsi_turn_secret: "{{ turnserver_auth_secret | default('p@ssw0rd') }}" # Authentication. Can be set to -# * False : no authentication at all (can also be None) +# * false : no authentication at all (can also be None) # * sso : In this case, you have to protect /login with your SSO system (through a reverse proxy) # And once authenticated, send the HTTP headers mail and displayName with the appropriate values # Note that jitsi Android client does not support sso authentication, so mobile users will be able # to join an existing conf, but not create one easily +# * token : to use JWT Tokens # * ldap : Will use an LDAP server for authentication. Works on mobile, but is a bit less convinient # than sso for desktop users. See all the jitsi_ldap_xxxx settings -jitsi_auth: False +jitsi_auth: false + +# If using token +jitsi_token_app_id: jitsi + +# Either jitsi_token_app_secret or jitsi_token_asap_key_server must be set +# jitsi_token_app_secret: XXXX +# jitsi_token_asap_key_server: https://sso.example.org/jitsi/asap + +jitsi_token_iss: https://sso.example.org +jitsi_token_aud: "{{ jitsi_token_app_id }}" +jitsi_token_auth_url: https://sso.example.org/jitsi/login?room={room} jitsi_jicofo_xmpp_user: focus jitsi_jicofo_xmpp_domain: "{{ jitsi_auth_domain }}" @@ -52,7 +64,7 @@ jitsi_auth_domain: auth.{{ jitsi_domain }} # Can be either true, in which case a cert will be automatically obtained using letsencrypt # or can be a name, in which case you have to configure letsencrypt to obtain the cert yourself -# jitsi_letsencrypt_cert: True +# jitsi_letsencrypt_cert: true # or # jitsi_letsencrypt_cert: jitsi.example.com # @@ -71,33 +83,32 @@ jitsi_meet_conf_base: websocket: wss://{{ jitsi_domain }}/xmpp-websocket clientNode: http://jitsi.org/jitsimeet focusUserJid: "{{ jitsi_jicofo_xmpp_user }}@{{ jitsi_auth_domain }}" - enableNoAudioDetection: True - enableNoisyMicDetection: True + enableNoAudioDetection: true + enableNoisyMicDetection: true startAudioMuted: 10 startVideoMuted: 10 - enableOpusRed: True - desktopSharingFrameRate: - min: 5 - max: 30 - channelLastN: 25 - enableLayerSuspension: True - enableUnifiedOnChrome: True - requireDisplayName: False + enableOpusRed: true + #desktopSharingFrameRate: + # min: 5 + # max: 30 + requireDisplayName: true prejoinConfig: - enabled: True - enableInsecureRoomNameWarning: False - disableThirdPartyRequests: True + enabled: true + enableInsecureRoomNameWarning: false + disableThirdPartyRequests: true welcomePage: - disabled: False + disabled: false + lobby: + enableChat: true localRecording: - notifyAllParticipants: True + notifyAllParticipants: true recordingService: - enabled: "{{ (jitsi_jibri_recordings_base_url is defined) | ternary(True, False) }}" + enabled: "{{ (jitsi_jibri_recordings_base_url is defined) | ternary(true, false) }}" p2p: - enabled: False - enableUnifiedOnChrome: True + enabled: false + enableUnifiedOnChrome: true analytics: - disabled: True + disabled: true toolbarButtons: - camera - chat @@ -129,18 +140,22 @@ jitsi_meet_conf_base: dialInNumbersUrl: https://{{ jitsi_domain }}/phoneNumberList dialInConfCodeUrl: https://{{ jitsi_domain }}/conferenceMapper screenshotCapture: - enabled: True + enabled: true transcription: - enabled: False - useTurnUdp: True + enabled: false + useTurnUdp: true defaultLanguage: fr gravatar: - disabled: True + disabled: true giphy: - enabled: True + enabled: true + breakoutRooms: + hideAddRoomButton: false + hideAutoAssignButton: true + hideJoinRoomButton: false jitsi_meet_conf_extra: {} -jitsi_meet_conf: "{{ jitsi_meet_conf_base | combine(jitsi_meet_conf_extra, recursive=True) }}" +jitsi_meet_conf: "{{ jitsi_meet_conf_base | combine(jitsi_meet_conf_extra, recursive=true) }}" # Meet interface configuration. Will be converted to JSON # See https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js for available settings and their meaning @@ -150,29 +165,29 @@ jitsi_meet_interface_conf_base: AUDIO_LEVEL_SECONDARY_COLOR: 'rgba(255,255,255,0.2)' AUTO_PIN_LATEST_SCREEN_SHARE: remote-only BRAND_WATERMARK_LINK: https://www.ehtrace.com - CLOSE_PAGE_GUEST_HINT: False + CLOSE_PAGE_GUEST_HINT: false DEFAULT_BACKGROUND: '#040404' DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.svg' - DISABLE_DOMINANT_SPEAKER_INDICATOR: False - DISABLE_JOIN_LEAVE_NOTIFICATIONS: False - DISABLE_PRESENCE_STATUS: False - DISABLE_RINGING: False - DISABLE_TRANSCRIPTION_SUBTITLES: True - DISABLE_VIDEO_BACKGROUND: False - DISPLAY_WELCOME_FOOTER: True - DISPLAY_WELCOME_PAGE_ADDITIONAL_CARD: False - DISPLAY_WELCOME_PAGE_CONTENT: False - DISPLAY_WELCOME_PAGE_TOOLBAR_ADDITIONAL_CONTENT: False - ENABLE_DIAL_OUT: "{{ (jitsi_jigasi_sip_server is defined) | ternary(True, False) }}" - ENABLE_FEEDBACK_ANIMATION: False + DISABLE_DOMINANT_SPEAKER_INDICATOR: false + DISABLE_JOIN_LEAVE_NOTIFICATIONS: false + DISABLE_PRESENCE_STATUS: false + DISABLE_RINGING: false + DISABLE_TRANSCRIPTION_SUBTITLES: true + DISABLE_VIDEO_BACKGROUND: false + DISPLAY_WELCOME_FOOTER: true + DISPLAY_WELCOME_PAGE_ADDITIONAL_CARD: false + DISPLAY_WELCOME_PAGE_CONTENT: false + DISPLAY_WELCOME_PAGE_TOOLBAR_ADDITIONAL_CONTENT: false + ENABLE_DIAL_OUT: "{{ (jitsi_jigasi_sip_server is defined) | ternary(true, false) }}" + ENABLE_FEEDBACK_ANIMATION: false FILM_STRIP_MAX_HEIGHT: 120 - GENERATE_ROOMNAMES_ON_WELCOME_PAGE: True - HIDE_INVITE_MORE_HEADER: False + GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true + HIDE_INVITE_MORE_HEADER: false JITSI_WATERMARK_LINK: https://www.ehtrace.com - LANG_DETECTION: True + LANG_DETECTION: true LOCAL_THUMBNAIL_RATIO: 16 / 9 MAXIMUM_ZOOMING_COEFFICIENT: 1.3 - MOBILE_APP_PROMO: True + MOBILE_APP_PROMO: true OPTIMAL_BROWSERS: - chrome - chromium @@ -182,7 +197,7 @@ jitsi_meet_interface_conf_base: - safari POLICY_LOGO: null PROVIDER_NAME: Ehtrace - RECENT_LIST_ENABLED: True + RECENT_LIST_ENABLED: true REMOTE_THUMBNAIL_RATIO: 1 SETTINGS_SECTIONS: - devices @@ -191,19 +206,19 @@ jitsi_meet_interface_conf_base: - profile - sounds - more - SHOW_BRAND_WATERMARK: False - SHOW_CHROME_EXTENSION_BANNER: False - SHOW_JITSI_WATERMARK: False - SHOW_POWERED_BY: False - SHOW_PROMOTIONAL_CLOSE_PAGE: False + SHOW_BRAND_WATERMARK: false + SHOW_CHROME_EXTENSION_BANNER: false + SHOW_JITSI_WATERMARK: false + SHOW_POWERED_BY: false + SHOW_PROMOTIONAL_CLOSE_PAGE: false SUPPORT_URL: 'mailto:support@ehtrace.com' UNSUPPORTED_BROWSERS: [] - VERTICAL_FILMSTRIP: True + VERTICAL_FILMSTRIP: true VIDEO_LAYOUT_FIT: both - VIDEO_QUALITY_LABEL_DISABLED: False + VIDEO_QUALITY_LABEL_DISABLED: false jitsi_meet_interface_conf_extra: {} -jitsi_meet_interface_conf: "{{ jitsi_meet_interface_conf_base | combine(jitsi_meet_interface_conf_extra, recursive=True) }}" +jitsi_meet_interface_conf: "{{ jitsi_meet_interface_conf_base | combine(jitsi_meet_interface_conf_extra, recursive=true) }}" # You can customize strings here (lang/main-XX.json) jitsi_meet_custom_lang: {} @@ -221,12 +236,12 @@ jitsi_meet_custom_lang: {} # If jitsi_auth is ldap # We inherit values from prosody if available, or we try to get values from ad_auth or ldap_auth -jitsi_ldap_base: "{{ prosody_ldap_base | default(ad_auth | default(False) | ternary((ad_ldap_user_search_base is defined) | ternary(ad_ldap_user_search_base,'DC=' + ad_realm | default(samba_realm) | default(ansible_domain) | regex_replace('\\.',',DC=')), ldap_user_base | default('ou=Users') + ',' + ldap_base | default(ansible_domain | regex_replace('\\.','dc=')))) }}" -jitsi_ldap_servers: "{{ prosody_ldap_server | default(ad_ldap_servers | default([ad_auth | default(False) | ternary(ad_realm | default(samba_realm) | default(ansible_domain) | lower, ldap_uri | default('ldap://' + ansible_domain) | urlsplit('hostname'))]))}}" +jitsi_ldap_base: "{{ prosody_ldap_base | default(ad_auth | default(false) | ternary((ad_ldap_user_search_base is defined) | ternary(ad_ldap_user_search_base,'DC=' + ad_realm | default(samba_realm) | default(ansible_domain) | regex_replace('\\.',',DC=')), ldap_user_base | default('ou=Users') + ',' + ldap_base | default(ansible_domain | regex_replace('\\.','dc=')))) }}" +jitsi_ldap_servers: "{{ prosody_ldap_server | default(ad_ldap_servers | default([ad_auth | default(false) | ternary(ad_realm | default(samba_realm) | default(ansible_domain) | lower, ldap_uri | default('ldap://' + ansible_domain) | urlsplit('hostname'))]))}}" jitsi_ldap_bind_dn: "{{ prosody_ldap_bind_dn | default(None) }}" jitsi_ldap_bind_pass: "{{ prosody_ldap_bind_pass | default(None) }}" -jitsi_ldap_filter: "{{ prosody_ldap_filter | default(ad_auth | default(False) | ternary('(&(objectClass=user)(sAMAccountName=%s))','(&(objectClass=inetOrgPerson)(uid=%s))')) }}" -jitsi_ldap_starttls: "{{ prosody_ldap_starttls | default(True) }}" +jitsi_ldap_filter: "{{ prosody_ldap_filter | default(ad_auth | default(false) | ternary('(&(objectClass=user)(sAMAccountName=%s))','(&(objectClass=inetOrgPerson)(uid=%s))')) }}" +jitsi_ldap_starttls: "{{ prosody_ldap_starttls | default(true) }}" # Jigasi settings @@ -267,7 +282,7 @@ jitsi_jigasi_sip_extra_conf: {} # ENCRYPTION_PROTOCOL_STATUS.ZRTP: 'false' # IS_PRESENCE_ENABLED: 'true' # SDES_CIPHER_SUITES: AES_CM_128_HMAC_SHA1_80,AES_CM_128_HMAC_SHA1_32 -jitsi_jigasi_sip_conf: "{{ jitsi_jigasi_sip_base_conf | combine(jitsi_jigasi_sip_extra_conf, recursive=True) }}" +jitsi_jigasi_sip_conf: "{{ jitsi_jigasi_sip_base_conf | combine(jitsi_jigasi_sip_extra_conf, recursive=true) }}" jitsi_jigasi_xmpp_user: jigasi jitsi_jigasi_xmpp_domain: "{{ jitsi_auth_domain }}" @@ -291,7 +306,7 @@ jitsi_confmapper_conf_base: id_max_length: 4 db_file: "{{ jitsi_root_dir }}/data/confmapper/confmapper.sqlite" jitsi_confmapper_conf_extra: {} -jitsi_confmapper_conf: "{{ jitsi_confmapper_conf_base | combine(jitsi_confmapper_conf_extra, recursive=True) }}" +jitsi_confmapper_conf: "{{ jitsi_confmapper_conf_base | combine(jitsi_confmapper_conf_extra, recursive=true) }}" # This is for Jibri integration jitsi_jibri_xmpp_user: jibri diff --git a/roles/jitsi/tasks/facts.yml b/roles/jitsi/tasks/facts.yml index 2c0cd6b..d22dca6 100644 --- a/roles/jitsi/tasks/facts.yml +++ b/roles/jitsi/tasks/facts.yml @@ -73,10 +73,19 @@ - set_fact: jitsi_anonymousdomain: hosts: - anonymousdomain: guest.{{ jitsi_domain }} - - set_fact: jitsi_meet_conf={{ jitsi_anonymousdomain | combine(jitsi_meet_conf, recursive=True) }} - when: jitsi_auth == 'ldap' - tags: jisti + anonymousdomain: guest.{{ jitsi_domain }} + - set_fact: jitsi_meet_conf={{ jitsi_meet_conf | combine(jitsi_anonymousdomain, recursive=True) }} + when: jitsi_auth == 'ldap' or jitsi_auth == 'token' + tags: jitsi + +- name: Set authentication url for jitsi meet + block: + - set_fact: + jitsi_authurl: + tokenAuthUrl: '{{ jitsi_token_auth_url }}' + - set_fact: jitsi_meet_conf={{ jitsi_meet_conf | combine(jitsi_authurl, recursive=True) }} + when: jitsi_auth == 'token' + tags: jitsi - name: Check if cert file exist stat: path={{ jitsi_cert_path }} diff --git a/roles/jitsi/templates/jicofo/jicofo.conf.j2 b/roles/jitsi/templates/jicofo/jicofo.conf.j2 index b523e7b..e5d418a 100644 --- a/roles/jitsi/templates/jicofo/jicofo.conf.j2 +++ b/roles/jitsi/templates/jicofo/jicofo.conf.j2 @@ -2,8 +2,17 @@ jicofo { authentication { - enabled = {{ (jitsi_auth == 'sso' or jitsi_auth == 'ldap') | ternary('true', 'false') }} - type = {{ (jitsi_auth == 'ldap') | ternary('XMPP', 'SHIBBOLETH') }} +{% if jitsi_auth == 'sso' %} + enabled = true + type = SHIBBOLETH +{% elif jitsi_auth == 'ldap' %} + enabled = true + type = XMPP +{% elif jitsi_auth == 'token' %} + enabled = true + type = JWT + login-url = {{ jitsi_domain }} +{% endif %} } bridge { @@ -28,5 +37,6 @@ jicofo { password = "{{ jitsi_jicofo_xmpp_pass }}" client-proxy = focus.{{ jitsi_domain }} } + trusted-domains = ["{{ jitsi_jibri_xmpp_domain | default('recorder.' ~ jitsi_jibri_domain) }}"] } } diff --git a/roles/jitsi/templates/prosody.cfg.lua.j2 b/roles/jitsi/templates/prosody.cfg.lua.j2 index fdefb53..916606a 100644 --- a/roles/jitsi/templates/prosody.cfg.lua.j2 +++ b/roles/jitsi/templates/prosody.cfg.lua.j2 @@ -11,7 +11,10 @@ external_services = { {% for stun in jitsi_stun_servers %} { type = "{{ stun | urlsplit('scheme') }}", - host = "{{ stun | regex_replace('(turns?|stun):([^:]+)(:\d+)?.*', '\\2') }}{% if stun | regex_replace('(turns?|stun):.+:(\d+)?.*', '\\2') | int > 0 and stun | regex_replace('(turns?|stun):.+:(\d+)?.*', '\\2') | int < 65535 %}:{{ stun | regex_replace('(turns?|stun):.+:(\d+)?.*', '\\2') }}{% endif %}", + host = "{{ stun | regex_replace('(turns?|stun):([^:]+)(:\d+)?.*', '\\2') }}", +{% if stun | regex_replace('(turns?|stun):.+:(\d+)?.*', '\\2') | int > 0 and stun | regex_replace('(turns?|stun):.+:(\d+)?.*', '\\2') | int < 65535 %} + port = {{ stun | regex_replace('(turns?|stun):.+:(\d+)?.*', '\\2') }}, +{% endif %} {% if stun | urlsplit('query') is search('transport=') %} transport = "{{ stun | urlsplit('query') | regex_replace('.*transport=(udp|tcp).*', '\\1') }}", {% endif %} @@ -24,13 +27,13 @@ external_services = { {% endfor %} }; -cross_domain_bosh = false; +-- cross_domain_bosh = false; cross_domain_websocket = true; consider_bosh_secure = true; unlimited_jids = { - "{{ jitsi_jicofo_xmpp_user }}@{{ jitsi_auth_domain }}", - "{{ jitsi_videobridge_xmpp_user }}@{{ jitsi_videobridge_xmpp_domain }}" + "{{ jitsi_jicofo_xmpp_user }}@{{ jitsi_auth_domain }}", + "{{ jitsi_videobridge_xmpp_user }}@{{ jitsi_videobridge_xmpp_domain }}" } VirtualHost "{{ jitsi_domain }}" @@ -45,40 +48,59 @@ VirtualHost "{{ jitsi_domain }}" ldap_filter = "{{ jitsi_ldap_filter }}" ldap_scope = "subtree" ldap_tls = {{ jitsi_ldap_starttls | ternary('true','false') }} +{% elif jitsi_auth == 'token' %} + authentication = "token" + app_id = "{{ jitsi_token_app_id }}"; + asap_accepted_issuers = "{{ jitsi_token_iss }}"; + asap_accepted_audiences = "{{ jitsi_token_aud }}"; +{% if jitsi_token_app_secret is defined %} + app_secret = "{{ jitsi_token_app_secret }}"; +{% elif jitsi_token_asap_key_server is defined %} + asap_key_server = "{{ jitsi_token_asap_key_server }}"; +{% endif %} + allow_empty_token = false; {% else %} - authentication = "anonymous" + authentication = "jitsi-anonymous" {% endif %} ssl = { key = "{{ jitsi_key_path }}"; certificate = "{{ jitsi_cert_path }}"; } + c2s_require_encryption = false + allow_unencrypted_plain_auth = true + av_moderation_component = "avmoderation.{{ jitsi_domain }}" + speakerstats_component = "speakerstats.{{ jitsi_domain }}" + end_conference_component = "endconference.{{ jitsi_domain }}" + modules_enabled = { "bosh"; "pubsub"; - "ping"; "websocket"; - "external_services"; + "ping"; "speakerstats"; + "external_services"; "conference_duration"; + "end_conference"; "muc_lobby_rooms"; - "participant_metadata"; "muc_breakout_rooms"; "av_moderation"; + "room_metadata"; + "participant_metadata"; + "presence_identity"; } - c2s_require_encryption = false - allow_unencrypted_plain_auth = true - speakerstats_component = "speakerstats.{{ jitsi_domain }}" + conference_duration_component = "conferenceduration.{{ jitsi_domain }}" lobby_muc = "lobby.{{ jitsi_domain }}" breakout_rooms_muc = "breakout.{{ jitsi_domain }}" + room_metadata_component = "metadata.{{ jitsi_domain }}" main_muc = "conference.{{ jitsi_domain }}" muc_lobby_whitelist = { "recorder.{{ jitsi_domain }}" } -{% if jitsi_auth == 'ldap' %} +{% if jitsi_auth == 'ldap' or jitsi_auth == 'token' %} -- Guest virtual domain VirtualHost "guest.{{ jitsi_domain }}" - authentication = "anonymous" + authentication = "jitsi-anonymous" c2s_require_encryption = false modules_enabled = { "participant_metadata"; @@ -101,10 +123,12 @@ VirtualHost "recorder.{{ jitsi_domain }}" c2s_require_encryption = false Component "conference.{{ jitsi_domain }}" "muc" + restrict_room_creation = true storage = "memory" modules_enabled = { "ping"; "jibri_bypass_pwd"; + "muc_hide_all"; "muc_meeting_id"; "muc_domain_mapper"; "polls"; @@ -113,11 +137,19 @@ Component "conference.{{ jitsi_domain }}" "muc" admins = { "{{ jitsi_jicofo_xmpp_user }}@{{ jitsi_auth_domain }}" } muc_room_locking = false muc_room_default_public_jids = true + muc_password_whitelist = { + "{{ jitsi_jicofo_xmpp_user }}@{{ jitsi_auth_domain }}" + } Component "internal.{{ jitsi_auth_domain }}" "muc" storage = "memory" - modules_enabled = { "ping"; } - muc_room_cache_size = 1000 + modules_enabled = { + "muc_hide_all"; + "ping"; + } + admins = { "{{ jitsi_jicofo_xmpp_user }}@{{ jitsi_auth_domain }}" } + muc_room_locking = false + muc_room_default_public_jids = true Component "focus.{{ jitsi_domain }}" "client_proxy" target_address = "{{ jitsi_jicofo_xmpp_user }}@{{ jitsi_auth_domain }}" @@ -128,6 +160,9 @@ Component "speakerstats.{{ jitsi_domain }}" "speakerstats_component" Component "conferenceduration.{{ jitsi_domain }}" "conference_duration_component" muc_component = "conference.{{ jitsi_domain }}" +Component "endconference.{{ jitsi_domain }}" "end_conference" + muc_component = "conference.{{ jitsi_domain }}" + Component "avmoderation.{{ jitsi_domain }}" "av_moderation_component" muc_component = "conference.{{ jitsi_domain }}" @@ -137,16 +172,24 @@ Component "lobby.{{ jitsi_domain }}" "muc" muc_room_locking = false muc_room_default_public_jids = true modules_enabled = { + "muc_hide_all"; "muc_rate_limit"; + "polls"; } +Component "metadata.{{ jitsi_domain }}" "room_metadata_component" + muc_component = "conference.{{ jitsi_domain }}" + breakout_rooms_component = "breakout.{{ jitsi_domain }}" + Component "breakout.{{ jitsi_domain }}" "muc" restrict_room_creation = true storage = "memory" modules_enabled = { + "muc_hide_all"; "muc_meeting_id"; "muc_domain_mapper"; "muc_rate_limit"; + "polls"; } admins = { "{{ jitsi_jicofo_xmpp_user }}@{{ jitsi_auth_domain }}" } muc_room_locking = false diff --git a/roles/prosody/defaults/main.yml b/roles/prosody/defaults/main.yml index 02516cd..5dce63c 100644 --- a/roles/prosody/defaults/main.yml +++ b/roles/prosody/defaults/main.yml @@ -19,6 +19,24 @@ prosody_base_modules: - name: mod_auth_ldap - name: util.lib url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/util.lib.lua + - name: mod_auth_jitsi-anonymous + url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/mod_auth_jitsi-anonymous.lua + - name: mod_end_conference + url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/mod_end_conference.lua + - name: mod_room_metadata + url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/mod_room_metadata.lua + - name: mod_room_metadata_component + url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/mod_room_metadata_component.lua + - name: mod_muc_hide_all + url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/mod_muc_hide_all.lua + - name: mod_room_destroy + url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/mod_room_destroy.lua + - name: mod_presence_identity + url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/mod_presence_identity.lua + - name: luajwtjitsi.lib + url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/luajwtjitsi.lib.lua + - name: mod_auth_token + url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/mod_auth_token.lua - name: mod_speakerstats url: https://raw.githubusercontent.com/jitsi/jitsi-meet/master/resources/prosody-plugins/mod_speakerstats.lua - name: mod_speakerstats_component diff --git a/roles/prosody/files/prosody-ansible.te b/roles/prosody/files/prosody-ansible.te new file mode 100644 index 0000000..28f8130 --- /dev/null +++ b/roles/prosody/files/prosody-ansible.te @@ -0,0 +1,11 @@ + +module prosody-ansible 1.0; + +require { + type unlabeled_t; + type prosody_t; + class dir search; +} + +#============= prosody_t ============== +allow prosody_t unlabeled_t:dir search; diff --git a/roles/prosody/files/token/util.lib.lua b/roles/prosody/files/token/util.lib.lua new file mode 100644 index 0000000..cb23636 --- /dev/null +++ b/roles/prosody/files/token/util.lib.lua @@ -0,0 +1,539 @@ +-- Token authentication +-- Copyright (C) 2021-present 8x8, Inc. + +local basexx = require "basexx"; +local have_async, async = pcall(require, "util.async"); +local hex = require "util.hex"; +local jwt = module:require "luajwtjitsi"; +local jid = require "util.jid"; +local json_safe = require "cjson.safe"; +local path = require "util.paths"; +local sha256 = require "util.hashes".sha256; +local main_util = module:require "util"; +local ends_with = main_util.ends_with; +local http_get_with_retry = main_util.http_get_with_retry; +local extract_subdomain = main_util.extract_subdomain; +local starts_with = main_util.starts_with; +local table_shallow_copy = main_util.table_shallow_copy; +local cjson_safe = require 'cjson.safe' +local timer = require "util.timer"; +local async = require "util.async"; +-- local inspect = require 'inspect'; + +local nr_retries = 3; +local ssl = require "ssl"; + +-- TODO: Figure out a less arbitrary default cache size. +local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128); + +-- the cache for generated asap jwt tokens +local jwtKeyCache = require 'util.cache'.new(cacheSize); + +local ASAPTTL_THRESHOLD = module:get_option_number('asap_ttl_threshold', 600); +local ASAPTTL = module:get_option_number('asap_ttl', 3600); +local ASAPIssuer = module:get_option_string('asap_issuer', 'jitsi'); +local ASAPAudience = module:get_option_string('asap_audience', 'jitsi'); +local ASAPKeyId = module:get_option_string('asap_key_id', 'jitsi'); +local ASAPKeyPath = module:get_option_string('asap_key_path', '/etc/prosody/certs/asap.key'); + +local ASAPKey; +local f = io.open(ASAPKeyPath, 'r'); + +if f then + ASAPKey = f:read('*all'); + f:close(); +end + +local Util = {} +Util.__index = Util + +--- Constructs util class for token verifications. +-- Constructor that uses the passed module to extract all the +-- needed configurations. +-- If configuration is missing returns nil +-- @param module the module in which options to check for configs. +-- @return the new instance or nil +function Util.new(module) + local self = setmetatable({}, Util) + + self.appId = module:get_option_string("app_id"); + self.appSecret = module:get_option_string("app_secret"); + self.asapKeyServer = module:get_option_string("asap_key_server"); + -- A URL that will return json file with a mapping between kids and public keys + -- If the response Cache-Control header we will respect it and refresh it + self.cacheKeysUrl = module:get_option_string("cache_keys_url"); + self.signatureAlgorithm = module:get_option_string("signature_algorithm"); + self.allowEmptyToken = module:get_option_boolean("allow_empty_token"); + + self.cache = require"util.cache".new(cacheSize); + + --[[ + Multidomain can be supported in some deployments. In these deployments + there is a virtual conference muc, which address contains the subdomain + to use. Those deployments are accessible + by URL https://domain/subdomain. + Then the address of the room will be: + roomName@conference.subdomain.domain. This is like a virtual address + where there is only one muc configured by default with address: + conference.domain and the actual presentation of the room in that muc + component is [subdomain]roomName@conference.domain. + These setups relay on configuration 'muc_domain_base' which holds + the main domain and we use it to subtract subdomains from the + virtual addresses. + The following confgurations are for multidomain setups and domain name + verification: + --]] + + -- optional parameter for custom muc component prefix, + -- defaults to "conference" + self.muc_domain_prefix = module:get_option_string( + "muc_mapper_domain_prefix", "conference"); + -- domain base, which is the main domain used in the deployment, + -- the main VirtualHost for the deployment + self.muc_domain_base = module:get_option_string("muc_mapper_domain_base"); + -- The "real" MUC domain that we are proxying to + if self.muc_domain_base then + self.muc_domain = module:get_option_string( + "muc_mapper_domain", + self.muc_domain_prefix.."."..self.muc_domain_base); + end + -- whether domain name verification is enabled, by default it is enabled + -- when disabled checking domain name and tenant if available will be skipped, we will check only room name. + self.enableDomainVerification = module:get_option_boolean('enable_domain_verification', true); + + if self.allowEmptyToken == true then + module:log("warn", "WARNING - empty tokens allowed"); + end + + if self.appId == nil then + module:log("error", "'app_id' must not be empty"); + return nil; + end + + if self.appSecret == nil and self.asapKeyServer == nil then + module:log("error", "'app_secret' or 'asap_key_server' must be specified"); + return nil; + end + + -- Set defaults for signature algorithm + if self.signatureAlgorithm == nil then + if self.asapKeyServer ~= nil then + self.signatureAlgorithm = "RS256" + elseif self.appSecret ~= nil then + self.signatureAlgorithm = "HS256" + end + end + + --array of accepted issuers: by default only includes our appId + self.acceptedIssuers = module:get_option_array('asap_accepted_issuers',{self.appId}) + + --array of accepted audiences: by default only includes our appId + self.acceptedAudiences = module:get_option_array('asap_accepted_audiences',{'*'}) + + self.requireRoomClaim = module:get_option_boolean('asap_require_room_claim', true); + + if self.asapKeyServer and not have_async then + module:log("error", "requires a version of Prosody with util.async"); + return nil; + end + + if self.cacheKeysUrl then + self.cachedKeys = {}; + local update_keys_cache; + update_keys_cache = async.runner(function (name) + local content, code, cache_for; + content, code, cache_for = http_get_with_retry(self.cacheKeysUrl, nr_retries); + if content ~= nil then + local keys_to_delete = table_shallow_copy(self.cachedKeys); + -- Let's convert any certificate to public key + for k, v in pairs(cjson_safe.decode(content)) do + if starts_with(v, '-----BEGIN CERTIFICATE-----') then + self.cachedKeys[k] = ssl.loadcertificate(v):pubkey(); + -- do not clean this key if it already exists + keys_to_delete[k] = nil; + end + end + -- let's schedule the clean in an hour and a half, current tokens will be valid for an hour + timer.add_task(90*60, function () + for k, _ in pairs(keys_to_delete) do + self.cachedKeys[k] = nil; + end + end); + + if cache_for then + cache_for = tonumber(cache_for); + -- let's schedule new update 60 seconds before the cache expiring + if cache_for > 60 then + cache_for = cache_for - 60; + end + timer.add_task(cache_for, function () + update_keys_cache:run("update_keys_cache"); + end); + else + -- no cache header let's consider updating in 6hours + timer.add_task(6*60*60, function () + update_keys_cache:run("update_keys_cache"); + end); + end + else + module:log('warn', 'Failed to retrieve cached public keys code:%s', code); + -- failed let's retry in 30 seconds + timer.add_task(30, function () + update_keys_cache:run("update_keys_cache"); + end); + end + end); + update_keys_cache:run("update_keys_cache"); + end + + return self +end + +function Util:set_asap_key_server(asapKeyServer) + self.asapKeyServer = asapKeyServer; +end + +function Util:set_asap_accepted_issuers(acceptedIssuers) + self.acceptedIssuers = acceptedIssuers; +end + +function Util:set_asap_accepted_audiences(acceptedAudiences) + self.acceptedAudiences = acceptedAudiences; +end + +function Util:set_asap_require_room_claim(checkRoom) + self.requireRoomClaim = checkRoom; +end + +function Util:clear_asap_cache() + self.cache = require"util.cache".new(cacheSize); +end + +--- Returns the public key by keyID +-- @param keyId the key ID to request +-- @return the public key (the content of requested resource) or nil +function Util:get_public_key(keyId) + local content = self.cache:get(keyId); + local code; + if content == nil then + -- If the key is not found in the cache. + -- module:log("debug", "Cache miss for key: %s", keyId); + local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem'); + -- module:log("debug", "Fetching public key from: %s", keyurl); + content, code = http_get_with_retry(keyurl, nr_retries); + if content ~= nil then + self.cache:set(keyId, content); + else + if code == nil then + -- this is timout after nr_retries retries + module:log('warn', 'Timeout retrieving %s from %s', keyId, keyurl); + end + end + return content; + else + -- If the key is in the cache, use it. + -- module:log("debug", "Cache hit for key: %s", keyId); + return content; + end +end + +--- Verifies token and process needed values to be stored in the session. +-- Token is obtained from session.auth_token. +-- Stores in session the following values: +-- session.jitsi_meet_room - the room name value from the token +-- session.jitsi_meet_domain - the domain name value from the token +-- session.jitsi_meet_context_user - the user details from the token +-- session.jitsi_meet_context_room - the room details from the token +-- session.jitsi_meet_context_group - the group value from the token +-- session.jitsi_meet_context_features - the features value from the token +-- @param session the current session +-- @return false and error +function Util:process_and_verify_token(session) + if session.auth_token == nil then + if self.allowEmptyToken then + return true; + else + return false, "not-allowed", "token required"; + end + end + + local key; + if session.public_key then + -- We're using an public key stored in the session + -- module:log("debug","Public key was found on the session"); + key = session.public_key; + elseif self.asapKeyServer and session.auth_token ~= nil then + -- We're fetching an public key from an ASAP server + local dotFirst = session.auth_token:find("%."); + if not dotFirst then return false, "not-allowed", "Invalid token" end + local header, err = json_safe.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1))); + if err then + return false, "not-allowed", "bad token format"; + end + local kid = header["kid"]; + if kid == nil then + return false, "not-allowed", "'kid' claim is missing"; + end + local alg = header["alg"]; + if alg == nil then + return false, "not-allowed", "'alg' claim is missing"; + end + if alg.sub(alg,1,2) ~= "RS" then + return false, "not-allowed", "'kid' claim only support with RS family"; + end + + if self.cachedKeys and self.cachedKeys[kid] then + key = self.cachedKeys[kid]; + else + key = self:get_public_key(kid); + end + + if key == nil then + return false, "not-allowed", "could not obtain public key"; + end + elseif self.appSecret ~= nil then + -- We're using a symmetric secret + key = self.appSecret + end + + if key == nil then + return false, "not-allowed", "signature verification key is missing"; + end + + -- now verify the whole token + local claims, msg = jwt.verify( + session.auth_token, + self.signatureAlgorithm, + key, + self.acceptedIssuers, + self.acceptedAudiences + ) + if claims ~= nil then + if self.requireRoomClaim then + local roomClaim = claims["room"]; + if roomClaim == nil then + return false, "'room' claim is missing"; + end + end + + -- Binds room name to the session which is later checked on MUC join + session.jitsi_meet_room = claims["room"]; + -- Binds domain name to the session + session.jitsi_meet_domain = claims["sub"]; + + -- Binds the user details to the session if available + if claims["context"] ~= nil then + session.jitsi_meet_str_tenant = claims["context"]["tenant"]; + + if claims["context"]["user"] ~= nil then + session.jitsi_meet_context_user = claims["context"]["user"]; + end + + if claims["context"]["group"] ~= nil then + -- Binds any group details to the session + session.jitsi_meet_context_group = claims["context"]["group"]; + end + + if claims["context"]["features"] ~= nil then + -- Binds any features details to the session + session.jitsi_meet_context_features = claims["context"]["features"]; + end + if claims["context"]["room"] ~= nil then + session.jitsi_meet_context_room = claims["context"]["room"] + end + elseif claims["user_id"] then + session.jitsi_meet_context_user = {}; + session.jitsi_meet_context_user.id = claims["user_id"]; + end + + -- fire event that token has been verified and pass the session and the decoded token + prosody.events.fire_event('jitsi-authentication-token-verified', { + session = session; + claims = claims; + }); + + if session.contextRequired and claims["context"] == nil then + return false, "not-allowed", 'jwt missing required context claim'; + end + + return true; + else + return false, "not-allowed", msg; + end +end + +--- Verifies room name and domain if necessary. +-- Checks configs and if necessary checks the room name extracted from +-- room_address against the one saved in the session when token was verified. +-- Also verifies domain name from token against the domain in the room_address, +-- if enableDomainVerification is enabled. +-- @param session the current session +-- @param room_address the whole room address as received +-- @return returns true in case room was verified or there is no need to verify +-- it and returns false in case verification was processed +-- and was not successful +function Util:verify_room(session, room_address) + if self.allowEmptyToken and session.auth_token == nil then + --module:log("debug", "Skipped room token verification - empty tokens are allowed"); + return true; + end + + -- extract room name using all chars, except the not allowed ones + local room,_,_ = jid.split(room_address); + if room == nil then + log("error", + "Unable to get name of the MUC room ? to: %s", room_address); + return true; + end + + local auth_room = session.jitsi_meet_room; + if auth_room then + if type(auth_room) == 'string' then + auth_room = string.lower(auth_room); + else + module:log('warn', 'session.jitsi_meet_room not string: %s', auth_room); + end + end + if not self.enableDomainVerification then + -- if auth_room is missing, this means user is anonymous (no token for + -- its domain) we let it through, jicofo is verifying creation domain + if auth_room and (room ~= auth_room and not ends_with(room, ']'..auth_room)) and auth_room ~= '*' then + return false; + end + + return true; + end + + local room_address_to_verify = jid.bare(room_address); + local room_node = jid.node(room_address); + -- parses bare room address, for multidomain expected format is: + -- [subdomain]roomName@conference.domain + local target_subdomain, target_room = extract_subdomain(room_node); + + -- if we have '*' as room name in token, this means all rooms are allowed + -- so we will use the actual name of the room when constructing strings + -- to verify subdomains and domains to simplify checks + local room_to_check; + if auth_room == '*' then + -- authorized for accessing any room assign to room_to_check the actual + -- room name + if target_room ~= nil then + -- we are in multidomain mode and we were able to extract room name + room_to_check = target_room; + else + -- no target_room, room_address_to_verify does not contain subdomain + -- so we get just the node which is the room name + room_to_check = room_node; + end + else + -- no wildcard, so check room against authorized room from the token + if session.jitsi_meet_context_room and (session.jitsi_meet_context_room["regex"] == true or session.jitsi_meet_context_room["regex"] == "true") then + if target_room ~= nil then + -- room with subdomain + room_to_check = target_room:match(auth_room); + else + room_to_check = room_node:match(auth_room); + end + else + -- not a regex + room_to_check = auth_room; + end + -- module:log("debug", "room to check: %s", room_to_check) + if not room_to_check then + if not self.requireRoomClaim then + -- if we do not require to have the room claim, and it is missing + -- there is no point of continue and verifying the roomName and the tenant + return true; + end + + return false; + end + end + + if session.jitsi_meet_str_tenant + and string.lower(session.jitsi_meet_str_tenant) ~= session.jitsi_web_query_prefix then + module:log('warn', 'Tenant differs for user:%s group:%s url_tenant:%s token_tenant:%s', + session.jitsi_meet_context_user and session.jitsi_meet_context_user.id or '', + session.jitsi_meet_context_group, + session.jitsi_web_query_prefix, session.jitsi_meet_str_tenant); + session.jitsi_meet_tenant_mismatch = true; + end + + local auth_domain = string.lower(session.jitsi_meet_domain); + local subdomain_to_check; + if target_subdomain then + if auth_domain == '*' then + -- check for wildcard in JWT claim, allow access if found + subdomain_to_check = target_subdomain; + else + -- no wildcard in JWT claim, so check subdomain against sub in token + subdomain_to_check = auth_domain; + end + -- from this point we depend on muc_domain_base, + -- deny access if option is missing + if not self.muc_domain_base then + module:log("warn", "No 'muc_domain_base' option set, denying access!"); + return false; + end + + return room_address_to_verify == jid.join( + "["..subdomain_to_check.."]"..room_to_check, self.muc_domain); + else + if auth_domain == '*' then + -- check for wildcard in JWT claim, allow access if found + subdomain_to_check = self.muc_domain; + else + -- no wildcard in JWT claim, so check subdomain against sub in token + subdomain_to_check = self.muc_domain_prefix.."."..auth_domain; + end + -- we do not have a domain part (multidomain is not enabled) + -- verify with info from the token + return room_address_to_verify == jid.join(room_to_check, subdomain_to_check); + end +end + +function Util:generateAsapToken(audience) + if not ASAPKey then + module:log('warn', 'No ASAP Key read, asap key generation is disabled'); + return '' + end + + audience = audience or ASAPAudience + local t = os.time() + local err + local exp_key = 'asap_exp.'..audience + local token_key = 'asap_token.'..audience + local exp = jwtKeyCache:get(exp_key) + local token = jwtKeyCache:get(token_key) + + --if we find a token and it isn't too far from expiry, then use it + if token ~= nil and exp ~= nil then + exp = tonumber(exp) + if (exp - t) > ASAPTTL_THRESHOLD then + return token + end + end + + --expiry is the current time plus TTL + exp = t + ASAPTTL + local payload = { + iss = ASAPIssuer, + aud = audience, + nbf = t, + exp = exp, + } + + -- encode + local alg = 'RS256' + token, err = jwt.encode(payload, ASAPKey, alg, { kid = ASAPKeyId }) + if not err then + token = 'Bearer '..token + jwtKeyCache:set(exp_key, exp) + jwtKeyCache:set(token_key, token) + return token + else + return '' + end +end + +return Util; diff --git a/roles/prosody/tasks/install.yml b/roles/prosody/tasks/install.yml index c1002d6..ab68bab 100644 --- a/roles/prosody/tasks/install.yml +++ b/roles/prosody/tasks/install.yml @@ -1,18 +1,22 @@ --- - name: Install prosody - yum: + package: name: - prosody - lua-ldap - lua-cyrussasl + - lua-cjson + - lua-basexx + - lua-luaossl + - libjwt tags: prosody - name: Create systemd unit snippet dir file: path=/etc/systemd/system/prosody.service.d state=directory tags: prosody -- name: Install modules +- name: Install remote modules get_url: url: "{{ item.url | default('https://raw.githubusercontent.com/prosody-modules/' ~ item.name ~ '/master/' ~ item.name ~ '.lua') }}" dest: /opt/prosody/modules/{{ item.name }}.lua @@ -20,11 +24,14 @@ notify: restart prosody tags: prosody -- name: Install Participan Metadata module +- name: Install additional modules copy: - src: mod_participant_metadata.lua + src: "{{ item }}" dest: /opt/prosody/modules/ notify: restart prosody + loop: + - mod_participant_metadata.lua + - token tags: prosody - name: Remove useless unit override diff --git a/roles/prosody/tasks/main.yml b/roles/prosody/tasks/main.yml index b214b8c..0188bc9 100644 --- a/roles/prosody/tasks/main.yml +++ b/roles/prosody/tasks/main.yml @@ -9,6 +9,10 @@ - include_tasks: facts.yml tags: always +- include_tasks: selinux.yml + when: ansible_selinux.status == 'enabled' + tags: always + - include_tasks: conf.yml tags: always diff --git a/roles/prosody/tasks/selinux.yml b/roles/prosody/tasks/selinux.yml new file mode 100644 index 0000000..63a2382 --- /dev/null +++ b/roles/prosody/tasks/selinux.yml @@ -0,0 +1,28 @@ +--- + +- name: Set correct SELinux context + sefcontext: + target: "/opt/prosody(/.*)?" + setype: lib_t + seuser: system_u + state: present + tags: prosody + +- name: Restore SELinux context + command: restorecon -R /opt/prosody/modules + changed_when: false + tags: prosody + +- name: Copy SELinux policy + copy: src=prosody-ansible.te dest=/etc/selinux/targeted/local/ + register: prosody_selinux_policy + tags: prosody + +- name: Compile and load SELinux policy + shell: | + cd /etc/selinux/targeted/local/ + checkmodule -M -m -o prosody-ansible.mod prosody-ansible.te + semodule_package -o prosody-ansible.pp -m prosody-ansible.mod + semodule -i /etc/selinux/targeted/local/prosody-ansible.pp + when: prosody_selinux_policy.changed + tags: prosody