keyfile: add helper functions to record loaded UUID files

This code will be used later.

We want to remember which keyfiles are currently loaded (or hidden).

With the addition or multiple keyfile directories (soon), there are
two cases where this matters:

 - if there are multiple keyfiles which reference the same UUID,
   we can only load one of them. That is already a problem today
   with only one keyfile directory, where multiple files can reference
   the same UUID.
   The implementation will pick the file based on priorities (like
   the file modification date). However, the user may call explicitly
   call `nmcli connection load`. In that case, we cannot reload
   all files to find out whether the to be loaded file is hidden
   according to the defined priorities. We cannot do that, because we
   must not make decisions based on files on disk, which we are not told
   to reload. So, during a `nmcli connection load` we must look at
   unrelated files, to determine how to load the file.
   Instead, we do allow the user to load any file, even if it would be
   shadowed by other files. When we do that, we may want to persist which
   file is currently loaded, so that a service restart and a `nmcli connection
   reload` does not undo the load again. This can be later later be solved by
   writing a symlink

       "/var/run/NetworkManager/system-connections/.loaded-$UUID.nmkeyfile"

   which targets the currently active file.

 - if a profile was loaded from read-only persistant storage, the user
   may still delete the profile. We also need to remember the deletion
   of the file. That will be achieved by symlinking "/dev/null" as
   "/etc/NetworkManager/system-connections/.loaded-$UUID.nmkeyfile".

Add helper functions to read and write these symlinks.
This commit is contained in:
Thomas Haller 2018-10-01 00:42:12 +02:00
parent f7de10ac83
commit 3fc5765e1b
5 changed files with 369 additions and 18 deletions

View file

@ -175,6 +175,10 @@ gboolean _nm_keyfile_has_values (GKeyFile *keyfile);
#define NM_KEYFILE_PATH_SUFFIX_NMCONNECTION ".nmconnection"
#define NM_KEYFILE_PATH_PREFIX_NMLOADED ".loaded-"
#define NM_KEYFILE_PATH_NMLOADED_NULL "/dev/null"
gboolean nm_keyfile_utils_ignore_filename (const char *filename, gboolean require_extension);
char *nm_keyfile_utils_create_filename (const char *filename, gboolean with_extension);

View file

@ -172,7 +172,8 @@ nms_keyfile_reader_from_file (const char *full_filename,
nm_assert (full_filename && full_filename[0] == '/');
nm_assert (!profile_dir || profile_dir[0] == '/');
if (!nms_keyfile_utils_check_file_permissions (full_filename,
if (!nms_keyfile_utils_check_file_permissions (NMS_KEYFILE_FILETYPE_KEYFILE,
full_filename,
NULL,
error))
return NULL;

View file

@ -27,6 +27,7 @@
#include <sys/stat.h>
#include "nm-keyfile-internal.h"
#include "nm-utils.h"
#include "nm-setting-wired.h"
#include "nm-setting-wireless.h"
#include "nm-setting-wireless-security.h"
@ -34,17 +35,212 @@
/*****************************************************************************/
char *
nms_keyfile_loaded_uuid_filename (const char *dirname,
const char *uuid,
gboolean temporary)
{
char filename[250];
nm_assert (dirname && dirname[0] == '/');
nm_assert (uuid && nm_utils_is_uuid (uuid) && !strchr (uuid, '/'));
if (g_snprintf (filename,
sizeof (filename),
"%s%s%s%s",
NM_KEYFILE_PATH_PREFIX_NMLOADED,
uuid,
NM_KEYFILE_PATH_SUFFIX_NMCONNECTION,
temporary ? "~" : "") >= sizeof (filename)) {
/* valid uuids are limited in length. The buffer should always be large
* enough. */
nm_assert_not_reached ();
return NULL;
}
return g_build_filename (dirname, filename, NULL);
}
gboolean
nms_keyfile_utils_check_file_permissions_stat (const struct stat *st,
nms_keyfile_loaded_uuid_read (const char *dirname,
const char *filename,
char **out_full_filename,
char **out_uuid,
char **out_loaded_path)
{
const char *uuid;
const char *tmp;
gsize len;
gs_free char *full_filename = NULL;
gs_free char *ln = NULL;
nm_assert (dirname && dirname[0] == '/');
nm_assert (filename && filename[0] && !strchr (filename, '/'));
if (filename[0] != '.') {
/* the hidden-uuid filename must start with '.'. That is,
* so that it does not conflict with regular keyfiles according
* to nm_keyfile_utils_ignore_filename(). */
return FALSE;
}
len = strlen (filename);
if ( len <= NM_STRLEN (NM_KEYFILE_PATH_PREFIX_NMLOADED)
|| memcmp (filename, NM_KEYFILE_PATH_PREFIX_NMLOADED, NM_STRLEN (NM_KEYFILE_PATH_PREFIX_NMLOADED)) != 0) {
/* the filename does not have the right prefix. */
return FALSE;
}
tmp = &filename[NM_STRLEN (NM_KEYFILE_PATH_PREFIX_NMLOADED)];
len -= NM_STRLEN (NM_KEYFILE_PATH_PREFIX_NMLOADED);
if ( len <= NM_STRLEN (NM_KEYFILE_PATH_SUFFIX_NMCONNECTION)
|| memcmp (&tmp[len - NM_STRLEN (NM_KEYFILE_PATH_SUFFIX_NMCONNECTION)],
NM_KEYFILE_PATH_SUFFIX_NMCONNECTION,
NM_STRLEN (NM_KEYFILE_PATH_SUFFIX_NMCONNECTION)) != 0) {
/* the file does not have the right suffix. */
return FALSE;
}
len -= NM_STRLEN (NM_KEYFILE_PATH_SUFFIX_NMCONNECTION);
if (!NM_IN_SET (len, 36, 40)) {
/* the remaining part of the filename has not the right length to
* contain a UUID (according to nm_utils_is_uuid()). */
return FALSE;
}
uuid = nm_strndup_a (100, tmp, len, NULL);
if (!nm_utils_is_uuid (uuid))
return FALSE;
full_filename = g_build_filename (dirname, filename, NULL);
if (!nms_keyfile_utils_check_file_permissions (NMS_KEYFILE_FILETYPE_NMLOADED,
full_filename,
NULL,
NULL))
return FALSE;
ln = nm_utils_read_link_absolute (full_filename, NULL);
if (!ln)
return FALSE;
NM_SET_OUT (out_uuid, g_strdup (uuid));
NM_SET_OUT (out_full_filename, g_steal_pointer (&full_filename));
NM_SET_OUT (out_loaded_path, g_steal_pointer (&ln));
return TRUE;
}
gboolean
nms_keyfile_loaded_uuid_read_from_file (const char *full_filename,
char **out_dirname,
char **out_filename,
char **out_uuid,
char **out_loaded_path)
{
gs_free char *dirname = NULL;
gs_free char *filename = NULL;
nm_assert (full_filename && full_filename[0] == '/');
filename = g_path_get_basename (full_filename);
dirname = g_path_get_dirname (full_filename);
if (!nms_keyfile_loaded_uuid_read (dirname,
filename,
NULL,
out_uuid,
out_loaded_path))
return FALSE;
NM_SET_OUT (out_dirname, g_steal_pointer (&dirname));
NM_SET_OUT (out_filename, g_steal_pointer (&filename));
return TRUE;
}
gboolean
nms_keyfile_loaded_uuid_write (const char *dirname,
const char *uuid,
const char *loaded_path,
gboolean allow_relative,
char **out_full_filename)
{
gs_free char *full_filename_tmp = NULL;
gs_free char *full_filename = NULL;
nm_assert (dirname && dirname[0] == '/');
nm_assert (uuid && nm_utils_is_uuid (uuid) && !strchr (uuid, '/'));
nm_assert (!loaded_path || loaded_path[0] == '/');
full_filename_tmp = nms_keyfile_loaded_uuid_filename (dirname, uuid, TRUE);
nm_assert (g_str_has_suffix (full_filename_tmp, "~"));
nm_assert (nm_utils_file_is_in_path (full_filename_tmp, dirname));
(void) unlink (full_filename_tmp);
if (!loaded_path) {
gboolean success = TRUE;
full_filename_tmp[strlen (full_filename_tmp) - 1] = '\0';
if (unlink (full_filename_tmp) != 0)
success = NM_IN_SET (errno, ENOENT);
NM_SET_OUT (out_full_filename, g_steal_pointer (&full_filename_tmp));
return success;
}
if (allow_relative) {
const char *f;
f = nm_utils_file_is_in_path (loaded_path, dirname);
if (f) {
/* @loaded_path points to a file directly in @dirname.
* Don't use absolute paths. */
loaded_path = f;
}
}
if (symlink (loaded_path, full_filename_tmp) != 0) {
full_filename_tmp[strlen (full_filename_tmp) - 1] = '\0';
NM_SET_OUT (out_full_filename, g_steal_pointer (&full_filename_tmp));
return FALSE;
}
full_filename = g_strdup (full_filename_tmp);
full_filename[strlen (full_filename) - 1] = '\0';
if (rename (full_filename_tmp, full_filename) != 0) {
(void) unlink (full_filename_tmp);
NM_SET_OUT (out_full_filename, g_steal_pointer (&full_filename));
return FALSE;
}
NM_SET_OUT (out_full_filename, g_steal_pointer (&full_filename));
return TRUE;
}
/*****************************************************************************/
gboolean
nms_keyfile_utils_check_file_permissions_stat (NMSKeyfileFiletype filetype,
const struct stat *st,
GError **error)
{
g_return_val_if_fail (st, FALSE);
if (!S_ISREG (st->st_mode)) {
g_set_error_literal (error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_INVALID_CONNECTION,
"file is not a regular file");
return FALSE;
}
if (filetype == NMS_KEYFILE_FILETYPE_KEYFILE) {
if (!S_ISREG (st->st_mode)) {
g_set_error_literal (error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_INVALID_CONNECTION,
"file is not a regular file");
return FALSE;
}
} else if (filetype == NMS_KEYFILE_FILETYPE_NMLOADED) {
if (!S_ISLNK (st->st_mode)) {
g_set_error_literal (error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_INVALID_CONNECTION,
"file is not a slink");
return FALSE;
}
} else
g_return_val_if_reached (FALSE);
if (!NM_FLAGS_HAS (nm_utils_get_testing (), NM_UTILS_TEST_NO_KEYFILE_OWNER_CHECK)) {
if (st->st_uid != 0) {
@ -54,7 +250,8 @@ nms_keyfile_utils_check_file_permissions_stat (const struct stat *st,
return FALSE;
}
if (st->st_mode & 0077) {
if ( filetype == NMS_KEYFILE_FILETYPE_KEYFILE
&& (st->st_mode & 0077)) {
g_set_error (error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_INVALID_CONNECTION,
"File permissions (%03o) are insecure",
st->st_mode);
@ -66,7 +263,8 @@ nms_keyfile_utils_check_file_permissions_stat (const struct stat *st,
}
gboolean
nms_keyfile_utils_check_file_permissions (const char *filename,
nms_keyfile_utils_check_file_permissions (NMSKeyfileFiletype filetype,
const char *filename,
struct stat *out_st,
GError **error)
{
@ -75,14 +273,24 @@ nms_keyfile_utils_check_file_permissions (const char *filename,
g_return_val_if_fail (filename && filename[0] == '/', FALSE);
if (stat (filename, &st) != 0) {
errsv = errno;
g_set_error (error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_INVALID_CONNECTION,
"cannot access file: %s", g_strerror (errsv));
return FALSE;
}
if (filetype == NMS_KEYFILE_FILETYPE_KEYFILE) {
if (stat (filename, &st) != 0) {
errsv = errno;
g_set_error (error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_INVALID_CONNECTION,
"cannot access file: %s", g_strerror (errsv));
return FALSE;
}
} else if (filetype == NMS_KEYFILE_FILETYPE_NMLOADED) {
if (lstat (filename, &st) != 0) {
errsv = errno;
g_set_error (error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_INVALID_CONNECTION,
"cannot access file: %s", g_strerror (errsv));
return FALSE;
}
} else
g_return_val_if_reached (FALSE);
if (!nms_keyfile_utils_check_file_permissions_stat (&st, error))
if (!nms_keyfile_utils_check_file_permissions_stat (filetype, &st, error))
return FALSE;
NM_SET_OUT (out_st, st);

View file

@ -29,13 +29,46 @@
#define NMS_KEYFILE_CONNECTION_LOG_FMTD "%s (%s,\"%s\",%p)"
#define NMS_KEYFILE_CONNECTION_LOG_ARGD(con) NMS_KEYFILE_CONNECTION_LOG_PATH (nm_settings_connection_get_filename ((NMSettingsConnection *) (con))), nm_settings_connection_get_uuid ((NMSettingsConnection *) (con)), nm_settings_connection_get_id ((NMSettingsConnection *) (con)), (con)
typedef enum {
NMS_KEYFILE_FILETYPE_KEYFILE,
NMS_KEYFILE_FILETYPE_NMLOADED,
} NMSKeyfileFiletype;
const char *nms_keyfile_utils_get_path (void);
/*****************************************************************************/
char *nms_keyfile_loaded_uuid_filename (const char *dirname,
const char *uuid,
gboolean temporary);
gboolean nms_keyfile_loaded_uuid_read (const char *dirname,
const char *filename,
char **out_full_filename,
char **out_uuid,
char **out_loaded_path);
gboolean nms_keyfile_loaded_uuid_read_from_file (const char *full_filename,
char **out_dirname,
char **out_filename,
char **out_uuid,
char **out_loaded_path);
gboolean nms_keyfile_loaded_uuid_write (const char *dirname,
const char *uuid,
const char *loaded_path,
gboolean allow_relative,
char **out_full_filename);
/*****************************************************************************/
struct stat;
gboolean nms_keyfile_utils_check_file_permissions_stat (const struct stat *st,
gboolean nms_keyfile_utils_check_file_permissions_stat (NMSKeyfileFiletype filetype,
const struct stat *st,
GError **error);
gboolean nms_keyfile_utils_check_file_permissions (const char *filename,
gboolean nms_keyfile_utils_check_file_permissions (NMSKeyfileFiletype filetype,
const char *filename,
struct stat *out_st,
GError **error);

View file

@ -2511,6 +2511,109 @@ test_nm_keyfile_plugin_utils_escape_filename (void)
/*****************************************************************************/
static void
_assert_keyfile_loaded_uuid (const char *dirname,
const char *uuid,
const char *loaded_path,
gboolean allow_relative,
const char *exp_full_filename,
const char *exp_uuid,
const char *exp_symlink_target,
const char *exp_loaded_path)
{
gs_free char *full_filename = NULL;
gs_free char *symlink_target = NULL;
gs_free char *uuid2 = NULL;
gs_free char *loaded_path2 = NULL;
gs_free char *dirname3 = NULL;
gs_free char *filename3 = NULL;
gs_free char *uuid3 = NULL;
gs_free char *loaded_path3 = NULL;
gboolean success;
gs_free char *filename = NULL;
g_assert (dirname && dirname[0] == '/');
g_assert (exp_full_filename && exp_full_filename[0]);
g_assert (!exp_loaded_path || exp_loaded_path[0] == '/');
filename = g_path_get_basename (exp_full_filename);
full_filename = nms_keyfile_loaded_uuid_filename (dirname, uuid, FALSE);
g_assert_cmpstr (full_filename, ==, full_filename);
nm_clear_g_free (&full_filename);
g_assert (nms_keyfile_loaded_uuid_write (dirname, uuid, loaded_path, allow_relative, &full_filename));
g_assert_cmpstr (full_filename, ==, exp_full_filename);
nm_clear_g_free (&full_filename);
if (exp_symlink_target)
g_assert (g_file_test (exp_full_filename, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_SYMLINK));
else
g_assert (!g_file_test (exp_full_filename, G_FILE_TEST_EXISTS));
symlink_target = g_file_read_link (exp_full_filename, NULL);
g_assert_cmpstr (symlink_target, ==, exp_symlink_target);
success = nms_keyfile_loaded_uuid_read (dirname, filename, &full_filename, &uuid2, &loaded_path2);
g_assert_cmpint (!!exp_uuid, ==, success);
if (success)
g_assert_cmpstr (full_filename, ==, exp_full_filename);
else
g_assert_cmpstr (full_filename, ==, NULL);
nm_clear_g_free (&full_filename);
g_assert_cmpstr (uuid2, ==, exp_uuid);
g_assert_cmpstr (loaded_path2, ==, exp_loaded_path);
success = nms_keyfile_loaded_uuid_read_from_file (exp_full_filename, &dirname3, &filename3, &uuid3, &loaded_path3);
g_assert_cmpint (!!exp_uuid, ==, success);
if (success) {
g_assert_cmpstr (dirname3, ==, dirname);
g_assert_cmpstr (filename3, ==, filename);
} else {
g_assert_cmpstr (dirname3, ==, NULL);
g_assert_cmpstr (filename3, ==, NULL);
}
g_assert_cmpstr (uuid3, ==, exp_uuid);
g_assert_cmpstr (loaded_path3, ==, exp_loaded_path);
}
static void
test_loaded_uuid (void)
{
const char *uuid = "3c03fd17-ddc3-4100-a954-88b6fafff959";
gs_free char *filename = g_strdup_printf ("%s%s%s",
NM_KEYFILE_PATH_PREFIX_NMLOADED,
uuid,
NM_KEYFILE_PATH_SUFFIX_NMCONNECTION);
gs_free char *full_filename = g_strdup_printf ("%s/%s",
TEST_SCRATCH_DIR,
filename);
const char *loaded_path0 = NM_KEYFILE_PATH_NMLOADED_NULL;
const char *loaded_path1 = "/some/where/but/not/scratch/dir";
const char *filename2 = "foo1";
gs_free char *loaded_path2 = g_strdup_printf ("%s/%s",
TEST_SCRATCH_DIR,
filename2);
_assert_keyfile_loaded_uuid (TEST_SCRATCH_DIR, uuid, NULL, FALSE, full_filename, NULL, NULL, NULL);
_assert_keyfile_loaded_uuid (TEST_SCRATCH_DIR, uuid, NULL, TRUE, full_filename, NULL, NULL, NULL);
_assert_keyfile_loaded_uuid (TEST_SCRATCH_DIR, uuid, loaded_path0, FALSE, full_filename, uuid, loaded_path0, loaded_path0);
_assert_keyfile_loaded_uuid (TEST_SCRATCH_DIR, uuid, loaded_path0, TRUE, full_filename, uuid, loaded_path0, loaded_path0);
_assert_keyfile_loaded_uuid (TEST_SCRATCH_DIR, uuid, loaded_path1, FALSE, full_filename, uuid, loaded_path1, loaded_path1);
_assert_keyfile_loaded_uuid (TEST_SCRATCH_DIR, uuid, loaded_path1, TRUE, full_filename, uuid, loaded_path1, loaded_path1);
_assert_keyfile_loaded_uuid (TEST_SCRATCH_DIR, uuid, loaded_path2, FALSE, full_filename, uuid, loaded_path2, loaded_path2);
_assert_keyfile_loaded_uuid (TEST_SCRATCH_DIR, uuid, loaded_path2, TRUE, full_filename, uuid, filename2, loaded_path2);
(void) unlink (full_filename);
}
/*****************************************************************************/
NMTST_DEFINE ();
int main (int argc, char **argv)
@ -2593,6 +2696,8 @@ int main (int argc, char **argv)
g_test_add_func ("/keyfile/test_nm_keyfile_plugin_utils_escape_filename", test_nm_keyfile_plugin_utils_escape_filename);
g_test_add_func ("/keyfile/test_loaded_uuid", test_loaded_uuid);
return g_test_run ();
}