mirror of
https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git
synced 2025-12-27 02:20:12 +01:00
secrets: merge branch 'elbs-unicon:fix_auth_retries'
https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/1381
This commit is contained in:
commit
11a34405ef
14 changed files with 120 additions and 69 deletions
|
|
@ -1172,8 +1172,10 @@ _con_get_try_complete_early(Request *req)
|
|||
}
|
||||
/* Do we have everything we need? */
|
||||
if (NM_FLAGS_HAS(req->con.get.flags, NM_SECRET_AGENT_GET_SECRETS_FLAG_ONLY_SYSTEM)
|
||||
|| ((nm_connection_need_secrets(tmp, NULL) == NULL)
|
||||
&& !NM_FLAGS_HAS(req->con.get.flags, NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW))) {
|
||||
|| (NM_FLAGS_HAS(req->con.get.flags, NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW)
|
||||
&& !nm_connection_need_secrets_for_rerequest(tmp))
|
||||
|| (!NM_FLAGS_HAS(req->con.get.flags, NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW)
|
||||
&& !nm_connection_need_secrets(tmp, NULL))) {
|
||||
_LOGD(NULL, "(" LOG_REQ_FMT ") system settings secrets sufficient", LOG_REQ_ARG(req));
|
||||
|
||||
/* Got everything, we're done */
|
||||
|
|
|
|||
|
|
@ -2333,6 +2333,45 @@ nm_connection_update_secrets(NMConnection *connection,
|
|||
return success;
|
||||
}
|
||||
|
||||
static const char *
|
||||
_need_secrets(NMConnection *connection, gboolean check_rerequest, GPtrArray **hints)
|
||||
{
|
||||
NMSetting *setting_before = NULL;
|
||||
NMConnectionPrivate *priv;
|
||||
int i;
|
||||
|
||||
nm_assert(NM_IS_CONNECTION(connection));
|
||||
nm_assert(!hints || !*hints);
|
||||
|
||||
priv = NM_CONNECTION_GET_PRIVATE(connection);
|
||||
|
||||
/* Get list of settings in priority order */
|
||||
for (i = 0; i < (int) _NM_META_SETTING_TYPE_NUM; i++) {
|
||||
NMSetting *setting = priv->settings[nm_meta_setting_types_by_priority[i]];
|
||||
GPtrArray *secrets;
|
||||
|
||||
if (!setting)
|
||||
continue;
|
||||
|
||||
nm_assert(!setting_before || _nm_setting_sort_for_nm_assert(setting_before, setting) < 0);
|
||||
nm_assert(!setting_before || _nm_setting_compare_priority(setting_before, setting) <= 0);
|
||||
setting_before = setting;
|
||||
|
||||
secrets = _nm_setting_need_secrets(setting, check_rerequest);
|
||||
if (!secrets)
|
||||
continue;
|
||||
|
||||
if (hints)
|
||||
*hints = secrets;
|
||||
else
|
||||
g_ptr_array_free(secrets, TRUE);
|
||||
|
||||
return nm_setting_get_name(setting);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* nm_connection_need_secrets:
|
||||
* @connection: the #NMConnection
|
||||
|
|
@ -2355,41 +2394,24 @@ nm_connection_update_secrets(NMConnection *connection,
|
|||
const char *
|
||||
nm_connection_need_secrets(NMConnection *connection, GPtrArray **hints)
|
||||
{
|
||||
NMSetting *setting_before = NULL;
|
||||
NMConnectionPrivate *priv;
|
||||
int i;
|
||||
|
||||
g_return_val_if_fail(NM_IS_CONNECTION(connection), NULL);
|
||||
if (hints)
|
||||
g_return_val_if_fail(*hints == NULL, NULL);
|
||||
g_return_val_if_fail(!hints || !*hints, NULL);
|
||||
|
||||
priv = NM_CONNECTION_GET_PRIVATE(connection);
|
||||
return _need_secrets(connection, FALSE, hints);
|
||||
}
|
||||
|
||||
/* Get list of settings in priority order */
|
||||
for (i = 0; i < (int) _NM_META_SETTING_TYPE_NUM; i++) {
|
||||
NMSetting *setting = priv->settings[nm_meta_setting_types_by_priority[i]];
|
||||
GPtrArray *secrets;
|
||||
/**
|
||||
* nm_connection_need_secrets_for_rerequest:
|
||||
* @connection: the #NMConnection
|
||||
*
|
||||
* Returns TRUE if some secret needs to be re-requested
|
||||
**/
|
||||
gboolean
|
||||
nm_connection_need_secrets_for_rerequest(NMConnection *connection)
|
||||
{
|
||||
g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE);
|
||||
|
||||
if (!setting)
|
||||
continue;
|
||||
|
||||
nm_assert(!setting_before || _nm_setting_sort_for_nm_assert(setting_before, setting) < 0);
|
||||
nm_assert(!setting_before || _nm_setting_compare_priority(setting_before, setting) <= 0);
|
||||
setting_before = setting;
|
||||
|
||||
secrets = _nm_setting_need_secrets(setting);
|
||||
if (!secrets)
|
||||
continue;
|
||||
|
||||
if (hints)
|
||||
*hints = secrets;
|
||||
else
|
||||
g_ptr_array_free(secrets, TRUE);
|
||||
|
||||
return nm_setting_get_name(setting);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
return !!_need_secrets(connection, TRUE, NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ _crypto_format_to_ck(NMCryptoFileFormat format)
|
|||
|
||||
/*****************************************************************************/
|
||||
|
||||
typedef void (*EAPMethodNeedSecretsFunc)(NMSetting8021x *self, GPtrArray *secrets, gboolean phase2);
|
||||
typedef void (*EAPMethodNeedSecretsFunc)(NMSetting8021x *self,
|
||||
GPtrArray *secrets,
|
||||
gboolean phase2,
|
||||
gboolean check_rerequest);
|
||||
|
||||
typedef gboolean (*EAPMethodValidateFunc)(NMSetting8021x *self, gboolean phase2, GError **error);
|
||||
|
||||
|
|
@ -2500,32 +2503,45 @@ nm_setting_802_1x_get_optional(NMSetting8021x *setting)
|
|||
/*****************************************************************************/
|
||||
|
||||
static void
|
||||
need_secrets_password(NMSetting8021x *self, GPtrArray *secrets, gboolean phase2)
|
||||
need_secrets_password(NMSetting8021x *self,
|
||||
GPtrArray *secrets,
|
||||
gboolean phase2,
|
||||
gboolean check_rerequest)
|
||||
{
|
||||
NMSetting8021xPrivate *priv = NM_SETTING_802_1X_GET_PRIVATE(self);
|
||||
|
||||
if (nm_str_is_empty(priv->password)
|
||||
&& (!priv->password_raw || !g_bytes_get_size(priv->password_raw))) {
|
||||
if (check_rerequest
|
||||
|| (nm_str_is_empty(priv->password)
|
||||
&& (!priv->password_raw || !g_bytes_get_size(priv->password_raw)))) {
|
||||
g_ptr_array_add(secrets, NM_SETTING_802_1X_PASSWORD);
|
||||
g_ptr_array_add(secrets, NM_SETTING_802_1X_PASSWORD_RAW);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
need_secrets_sim(NMSetting8021x *self, GPtrArray *secrets, gboolean phase2)
|
||||
need_secrets_sim(NMSetting8021x *self,
|
||||
GPtrArray *secrets,
|
||||
gboolean phase2,
|
||||
gboolean check_rerequest)
|
||||
{
|
||||
NMSetting8021xPrivate *priv = NM_SETTING_802_1X_GET_PRIVATE(self);
|
||||
|
||||
if (nm_str_is_empty(priv->pin))
|
||||
if (check_rerequest || nm_str_is_empty(priv->pin))
|
||||
g_ptr_array_add(secrets, NM_SETTING_802_1X_PIN);
|
||||
}
|
||||
|
||||
static void
|
||||
need_secrets_tls(NMSetting8021x *self, GPtrArray *secrets, gboolean phase2)
|
||||
need_secrets_tls(NMSetting8021x *self,
|
||||
GPtrArray *secrets,
|
||||
gboolean phase2,
|
||||
gboolean check_rerequest)
|
||||
{
|
||||
NMSetting8021xPrivate *priv = NM_SETTING_802_1X_GET_PRIVATE(self);
|
||||
NMSetting8021xCKScheme scheme;
|
||||
|
||||
/* If check_rerequest is TRUE do not return secrets, unless missing.
|
||||
* This secret cannot be wrong. */
|
||||
|
||||
if (!NM_FLAGS_HAS(phase2 ? priv->phase2_private_key_password_flags
|
||||
: priv->private_key_password_flags,
|
||||
NM_SETTING_SECRET_FLAG_NOT_REQUIRED)) {
|
||||
|
|
@ -2719,7 +2735,10 @@ verify_ttls(NMSetting8021x *self, gboolean phase2, GError **error)
|
|||
}
|
||||
|
||||
static void
|
||||
need_secrets_phase2(NMSetting8021x *self, GPtrArray *secrets, gboolean phase2)
|
||||
need_secrets_phase2(NMSetting8021x *self,
|
||||
GPtrArray *secrets,
|
||||
gboolean phase2,
|
||||
gboolean check_rerequest)
|
||||
{
|
||||
NMSetting8021xPrivate *priv = NM_SETTING_802_1X_GET_PRIVATE(self);
|
||||
char *method = NULL;
|
||||
|
|
@ -2740,7 +2759,7 @@ need_secrets_phase2(NMSetting8021x *self, GPtrArray *secrets, gboolean phase2)
|
|||
if (!eap_methods_table[i].ns_func)
|
||||
continue;
|
||||
if (nm_streq(eap_methods_table[i].method, method)) {
|
||||
(*eap_methods_table[i].ns_func)(self, secrets, TRUE);
|
||||
(*eap_methods_table[i].ns_func)(self, secrets, TRUE, check_rerequest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -3030,7 +3049,7 @@ verify(NMSetting *setting, NMConnection *connection, GError **error)
|
|||
/*****************************************************************************/
|
||||
|
||||
static GPtrArray *
|
||||
need_secrets(NMSetting *setting)
|
||||
need_secrets(NMSetting *setting, gboolean check_rerequest)
|
||||
{
|
||||
NMSetting8021x *self = NM_SETTING_802_1X(setting);
|
||||
NMSetting8021xPrivate *priv = NM_SETTING_802_1X_GET_PRIVATE(self);
|
||||
|
|
@ -3049,7 +3068,7 @@ need_secrets(NMSetting *setting)
|
|||
if (eap_methods_table[i].ns_func == NULL)
|
||||
continue;
|
||||
if (!strcmp(eap_methods_table[i].method, method)) {
|
||||
(*eap_methods_table[i].ns_func)(self, secrets, FALSE);
|
||||
(*eap_methods_table[i].ns_func)(self, secrets, FALSE, check_rerequest);
|
||||
|
||||
/* Only break out of the outer loop if this EAP method
|
||||
* needed secrets.
|
||||
|
|
|
|||
|
|
@ -225,12 +225,12 @@ verify_secrets(NMSetting *setting, NMConnection *connection, GError **error)
|
|||
}
|
||||
|
||||
static GPtrArray *
|
||||
need_secrets(NMSetting *setting)
|
||||
need_secrets(NMSetting *setting, gboolean check_rerequest)
|
||||
{
|
||||
NMSettingAdslPrivate *priv = NM_SETTING_ADSL_GET_PRIVATE(setting);
|
||||
GPtrArray *secrets = NULL;
|
||||
|
||||
if (priv->password && *priv->password)
|
||||
if (!check_rerequest && priv->password && *priv->password)
|
||||
return NULL;
|
||||
|
||||
if (!(priv->password_flags & NM_SETTING_SECRET_FLAG_NOT_REQUIRED)) {
|
||||
|
|
|
|||
|
|
@ -174,12 +174,12 @@ verify_secrets(NMSetting *setting, NMConnection *connection, GError **error)
|
|||
}
|
||||
|
||||
static GPtrArray *
|
||||
need_secrets(NMSetting *setting)
|
||||
need_secrets(NMSetting *setting, gboolean check_rerequest)
|
||||
{
|
||||
NMSettingCdmaPrivate *priv = NM_SETTING_CDMA_GET_PRIVATE(setting);
|
||||
GPtrArray *secrets = NULL;
|
||||
|
||||
if (!nm_str_is_empty(priv->password))
|
||||
if (!check_rerequest && !nm_str_is_empty(priv->password))
|
||||
return NULL;
|
||||
|
||||
if (priv->username) {
|
||||
|
|
|
|||
|
|
@ -460,12 +460,12 @@ verify_secrets(NMSetting *setting, NMConnection *connection, GError **error)
|
|||
}
|
||||
|
||||
static GPtrArray *
|
||||
need_secrets(NMSetting *setting)
|
||||
need_secrets(NMSetting *setting, gboolean check_rerequest)
|
||||
{
|
||||
NMSettingGsmPrivate *priv = NM_SETTING_GSM_GET_PRIVATE(setting);
|
||||
GPtrArray *secrets = NULL;
|
||||
|
||||
if (priv->password && *priv->password)
|
||||
if (!check_rerequest && priv->password && *priv->password)
|
||||
return NULL;
|
||||
|
||||
if (priv->username) {
|
||||
|
|
|
|||
|
|
@ -215,13 +215,13 @@ nm_setting_macsec_get_send_sci(NMSettingMacsec *setting)
|
|||
}
|
||||
|
||||
static GPtrArray *
|
||||
need_secrets(NMSetting *setting)
|
||||
need_secrets(NMSetting *setting, gboolean check_rerequest)
|
||||
{
|
||||
NMSettingMacsecPrivate *priv = NM_SETTING_MACSEC_GET_PRIVATE(setting);
|
||||
GPtrArray *secrets = NULL;
|
||||
|
||||
if (priv->mode == NM_SETTING_MACSEC_MODE_PSK) {
|
||||
if (!priv->mka_cak
|
||||
if ((check_rerequest || !priv->mka_cak)
|
||||
&& !NM_FLAGS_HAS(priv->mka_cak_flags, NM_SETTING_SECRET_FLAG_NOT_REQUIRED)) {
|
||||
secrets = g_ptr_array_sized_new(1);
|
||||
g_ptr_array_add(secrets, NM_SETTING_MACSEC_MKA_CAK);
|
||||
|
|
|
|||
|
|
@ -178,12 +178,12 @@ verify(NMSetting *setting, NMConnection *connection, GError **error)
|
|||
}
|
||||
|
||||
static GPtrArray *
|
||||
need_secrets(NMSetting *setting)
|
||||
need_secrets(NMSetting *setting, gboolean check_rerequest)
|
||||
{
|
||||
NMSettingPppoePrivate *priv = NM_SETTING_PPPOE_GET_PRIVATE(setting);
|
||||
GPtrArray *secrets = NULL;
|
||||
|
||||
if (priv->password)
|
||||
if (!check_rerequest && priv->password)
|
||||
return NULL;
|
||||
|
||||
if (!(priv->password_flags & NM_SETTING_SECRET_FLAG_NOT_REQUIRED)) {
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ struct _NMSettingClass {
|
|||
|
||||
gboolean (*verify_secrets)(NMSetting *setting, NMConnection *connection, GError **error);
|
||||
|
||||
GPtrArray *(*need_secrets)(NMSetting *setting);
|
||||
GPtrArray *(*need_secrets)(NMSetting *setting, gboolean check_rerequest);
|
||||
|
||||
int (*update_one_secret)(NMSetting *setting, const char *key, GVariant *value, GError **error);
|
||||
|
||||
|
|
@ -1046,7 +1046,7 @@ gboolean _nm_setting_use_legacy_property(NMSetting *setting,
|
|||
const char *legacy_property,
|
||||
const char *new_property);
|
||||
|
||||
GPtrArray *_nm_setting_need_secrets(NMSetting *setting);
|
||||
GPtrArray *_nm_setting_need_secrets(NMSetting *setting, gboolean check_rerequest);
|
||||
|
||||
gboolean _nm_setting_should_compare_secret_property(NMSetting *setting,
|
||||
NMSetting *other,
|
||||
|
|
|
|||
|
|
@ -801,7 +801,7 @@ set_secret_flags(NMSetting *setting,
|
|||
}
|
||||
|
||||
static GPtrArray *
|
||||
need_secrets(NMSetting *setting)
|
||||
need_secrets(NMSetting *setting, gboolean check_rerequest)
|
||||
{
|
||||
/* Assume that VPN connections need secrets since they almost always will */
|
||||
return g_ptr_array_sized_new(1);
|
||||
|
|
|
|||
|
|
@ -1800,13 +1800,13 @@ verify_secrets(NMSetting *setting, NMConnection *connection, GError **error)
|
|||
}
|
||||
|
||||
static GPtrArray *
|
||||
need_secrets(NMSetting *setting)
|
||||
need_secrets(NMSetting *setting, gboolean check_rerequest)
|
||||
{
|
||||
NMSettingWireGuardPrivate *priv = NM_SETTING_WIREGUARD_GET_PRIVATE(setting);
|
||||
GPtrArray *secrets = NULL;
|
||||
guint i;
|
||||
|
||||
if (!priv->private_key_valid) {
|
||||
if (check_rerequest || !priv->private_key_valid) {
|
||||
secrets = g_ptr_array_new_full(1, g_free);
|
||||
g_ptr_array_add(secrets, g_strdup(NM_SETTING_WIREGUARD_PRIVATE_KEY));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -815,7 +815,7 @@ nm_setting_wireless_security_get_fils(NMSettingWirelessSecurity *setting)
|
|||
}
|
||||
|
||||
static GPtrArray *
|
||||
need_secrets(NMSetting *setting)
|
||||
need_secrets(NMSetting *setting, gboolean check_rerequest)
|
||||
{
|
||||
NMSettingWirelessSecurity *self = NM_SETTING_WIRELESS_SECURITY(setting);
|
||||
NMSettingWirelessSecurityPrivate *priv = NM_SETTING_WIRELESS_SECURITY_GET_PRIVATE(self);
|
||||
|
|
@ -828,22 +828,22 @@ need_secrets(NMSetting *setting)
|
|||
/* Static WEP */
|
||||
if (strcmp(priv->key_mgmt, "none") == 0) {
|
||||
if ((priv->wep_tx_keyidx == 0)
|
||||
&& !nm_utils_wep_key_valid(priv->wep_key0, priv->wep_key_type)) {
|
||||
&& (check_rerequest || !nm_utils_wep_key_valid(priv->wep_key0, priv->wep_key_type))) {
|
||||
g_ptr_array_add(secrets, NM_SETTING_WIRELESS_SECURITY_WEP_KEY0);
|
||||
return secrets;
|
||||
}
|
||||
if ((priv->wep_tx_keyidx == 1)
|
||||
&& !nm_utils_wep_key_valid(priv->wep_key1, priv->wep_key_type)) {
|
||||
&& (check_rerequest || !nm_utils_wep_key_valid(priv->wep_key1, priv->wep_key_type))) {
|
||||
g_ptr_array_add(secrets, NM_SETTING_WIRELESS_SECURITY_WEP_KEY1);
|
||||
return secrets;
|
||||
}
|
||||
if ((priv->wep_tx_keyidx == 2)
|
||||
&& !nm_utils_wep_key_valid(priv->wep_key2, priv->wep_key_type)) {
|
||||
&& (check_rerequest || !nm_utils_wep_key_valid(priv->wep_key2, priv->wep_key_type))) {
|
||||
g_ptr_array_add(secrets, NM_SETTING_WIRELESS_SECURITY_WEP_KEY2);
|
||||
return secrets;
|
||||
}
|
||||
if ((priv->wep_tx_keyidx == 3)
|
||||
&& !nm_utils_wep_key_valid(priv->wep_key3, priv->wep_key_type)) {
|
||||
&& (check_rerequest || !nm_utils_wep_key_valid(priv->wep_key3, priv->wep_key_type))) {
|
||||
g_ptr_array_add(secrets, NM_SETTING_WIRELESS_SECURITY_WEP_KEY3);
|
||||
return secrets;
|
||||
}
|
||||
|
|
@ -852,7 +852,7 @@ need_secrets(NMSetting *setting)
|
|||
|
||||
/* WPA-PSK infrastructure */
|
||||
if (strcmp(priv->key_mgmt, "wpa-psk") == 0) {
|
||||
if (!nm_utils_wpa_psk_valid(priv->psk)) {
|
||||
if (check_rerequest || !nm_utils_wpa_psk_valid(priv->psk)) {
|
||||
g_ptr_array_add(secrets, NM_SETTING_WIRELESS_SECURITY_PSK);
|
||||
return secrets;
|
||||
}
|
||||
|
|
@ -861,7 +861,7 @@ need_secrets(NMSetting *setting)
|
|||
|
||||
/* SAE, used in MESH and WPA3-Personal */
|
||||
if (strcmp(priv->key_mgmt, "sae") == 0) {
|
||||
if (!priv->psk || !*priv->psk) {
|
||||
if (check_rerequest || !priv->psk || !*priv->psk) {
|
||||
g_ptr_array_add(secrets, NM_SETTING_WIRELESS_SECURITY_PSK);
|
||||
return secrets;
|
||||
}
|
||||
|
|
@ -870,7 +870,7 @@ need_secrets(NMSetting *setting)
|
|||
|
||||
/* LEAP */
|
||||
if (priv->auth_alg && !strcmp(priv->auth_alg, "leap") && !strcmp(priv->key_mgmt, "ieee8021x")) {
|
||||
if (!priv->leap_password || !*priv->leap_password) {
|
||||
if (check_rerequest || !priv->leap_password || !*priv->leap_password) {
|
||||
g_ptr_array_add(secrets, NM_SETTING_WIRELESS_SECURITY_LEAP_PASSWORD);
|
||||
return secrets;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3181,6 +3181,12 @@ _nm_setting_clear_secrets(NMSetting *setting,
|
|||
/**
|
||||
* _nm_setting_need_secrets:
|
||||
* @setting: the #NMSetting
|
||||
* @check_rerequest: If %TRUE: the stored secrets might be wrong and the agent
|
||||
* should query the user for the correct credentials. If an #NMSetting knows
|
||||
* that this cannot be the case it should *not* return the corresponding
|
||||
* setting object. Otherwise it should always return it, even if it is not
|
||||
* missing.
|
||||
* If %FALSE: only return it when it is missing.
|
||||
*
|
||||
* Returns an array of property names for each secret which may be required
|
||||
* to make a successful connection. The returned hints are only intended as a
|
||||
|
|
@ -3193,14 +3199,14 @@ _nm_setting_clear_secrets(NMSetting *setting,
|
|||
* free the elements.
|
||||
**/
|
||||
GPtrArray *
|
||||
_nm_setting_need_secrets(NMSetting *setting)
|
||||
_nm_setting_need_secrets(NMSetting *setting, gboolean check_rerequest)
|
||||
{
|
||||
GPtrArray *secrets = NULL;
|
||||
|
||||
g_return_val_if_fail(NM_IS_SETTING(setting), NULL);
|
||||
|
||||
if (NM_SETTING_GET_CLASS(setting)->need_secrets)
|
||||
secrets = NM_SETTING_GET_CLASS(setting)->need_secrets(setting);
|
||||
secrets = NM_SETTING_GET_CLASS(setting)->need_secrets(setting, check_rerequest);
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1051,4 +1051,6 @@ gboolean _nm_ip_tunnel_mode_is_layer2(NMIPTunnelMode mode);
|
|||
|
||||
GPtrArray *_nm_setting_ip_config_get_dns_array(NMSettingIPConfig *setting);
|
||||
|
||||
gboolean nm_connection_need_secrets_for_rerequest(NMConnection *connection);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue