Update to 2024-07-22 10:00

This commit is contained in:
Daniel Berteaud 2024-07-22 10:00:12 +02:00
parent 457d41ccf9
commit f5421b17f0
10 changed files with 770 additions and 86 deletions

View File

@ -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

View File

@ -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 }}

View File

@ -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) }}"]
}
}

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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