files_ops.c revision 1f49be4429c17475b789e9089ce4d0ae48315e74
/*
SSSD
Files provider operations
Copyright (C) 2016 Red Hat
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <dlfcn.h>
#include "config.h"
#include "providers/files/files_private.h"
#include "db/sysdb.h"
#include "util/inotify.h"
#include "util/util.h"
/* When changing this constant, make sure to also adjust the files integration
* test for reallocation branch
*/
#define FILES_REALLOC_CHUNK 64
#define PWD_MAXSIZE 1024
#define GRP_MAXSIZE 2048
struct files_ctx {
struct snotify_ctx *pwd_watch;
struct snotify_ctx *grp_watch;
struct files_ops_ctx *ops;
};
static errno_t enum_files_users(TALLOC_CTX *mem_ctx,
struct files_id_ctx *id_ctx,
struct passwd ***_users)
{
errno_t ret, close_ret;
struct passwd *pwd_iter = NULL;
struct passwd *pwd = NULL;
struct passwd **users = NULL;
FILE *pwd_handle = NULL;
size_t n_users = 0;
pwd_handle = fopen(id_ctx->passwd_file, "r");
if (pwd_handle == NULL) {
ret = errno;
DEBUG(SSSDBG_CRIT_FAILURE,
"Cannot open passwd file %s [%d]\n",
id_ctx->passwd_file, ret);
goto done;
}
users = talloc_zero_array(mem_ctx, struct passwd *,
FILES_REALLOC_CHUNK);
if (users == NULL) {
ret = ENOMEM;
goto done;
}
while ((pwd_iter = fgetpwent(pwd_handle)) != NULL) {
/* FIXME - we might want to support paging of sorts to avoid allocating
* all users atop a memory context or only return users that differ from
* the local storage as a diff to minimize memory spikes
*/
DEBUG(SSSDBG_TRACE_LIBS,
"User found (%s, %s, %"SPRIuid", %"SPRIgid", %s, %s, %s)\n",
pwd_iter->pw_name, pwd_iter->pw_passwd,
pwd_iter->pw_uid, pwd_iter->pw_gid,
pwd_iter->pw_gecos, pwd_iter->pw_dir,
pwd_iter->pw_shell);
pwd = talloc_zero(users, struct passwd);
if (pwd == NULL) {
ret = ENOMEM;
goto done;
}
pwd->pw_uid = pwd_iter->pw_uid;
pwd->pw_gid = pwd_iter->pw_gid;
pwd->pw_name = talloc_strdup(pwd, pwd_iter->pw_name);
if (pwd->pw_name == NULL) {
/* We only check pw_name here on purpose to allow broken
* records to be optionally rejected when saving them
* or fallback values to be used.
*/
ret = ENOMEM;
goto done;
}
pwd->pw_dir = talloc_strdup(pwd, pwd_iter->pw_dir);
pwd->pw_gecos = talloc_strdup(pwd, pwd_iter->pw_gecos);
pwd->pw_shell = talloc_strdup(pwd, pwd_iter->pw_shell);
pwd->pw_passwd = talloc_strdup(pwd, pwd_iter->pw_passwd);
users[n_users] = pwd;
n_users++;
if (n_users % FILES_REALLOC_CHUNK == 0) {
users = talloc_realloc(mem_ctx,
users,
struct passwd *,
talloc_array_length(users) + FILES_REALLOC_CHUNK);
if (users == NULL) {
ret = ENOMEM;
goto done;
}
}
}
ret = EOK;
users[n_users] = NULL;
*_users = users;
done:
if (ret != EOK) {
talloc_free(users);
}
if (pwd_handle) {
close_ret = fclose(pwd_handle);
if (close_ret != 0) {
close_ret = errno;
DEBUG(SSSDBG_CRIT_FAILURE,
"Cannot close passwd file %s [%d]\n",
id_ctx->passwd_file, close_ret);
}
}
return ret;
}
static errno_t enum_files_groups(TALLOC_CTX *mem_ctx,
struct files_id_ctx *id_ctx,
struct group ***_groups)
{
errno_t ret, close_ret;
struct group *grp_iter = NULL;
struct group *grp = NULL;
struct group **groups = NULL;
size_t n_groups = 0;
FILE *grp_handle = NULL;
grp_handle = fopen(id_ctx->group_file, "r");
if (grp_handle == NULL) {
ret = errno;
DEBUG(SSSDBG_CRIT_FAILURE,
"Cannot open group file %s [%d]\n",
id_ctx->group_file, ret);
goto done;
}
groups = talloc_zero_array(mem_ctx, struct group *,
FILES_REALLOC_CHUNK);
if (groups == NULL) {
ret = ENOMEM;
goto done;
}
while ((grp_iter = fgetgrent(grp_handle)) != NULL) {
DEBUG(SSSDBG_TRACE_LIBS,
"Group found (%s, %"SPRIgid")\n",
grp_iter->gr_name, grp_iter->gr_gid);
grp = talloc_zero(groups, struct group);
if (grp == NULL) {
ret = ENOMEM;
goto done;
}
grp->gr_gid = grp_iter->gr_gid;
grp->gr_name = talloc_strdup(grp, grp_iter->gr_name);
if (grp->gr_name == NULL) {
/* We only check gr_name here on purpose to allow broken
* records to be optionally rejected when saving them
* or fallback values to be used.
*/
ret = ENOMEM;
goto done;
}
grp->gr_passwd = talloc_strdup(grp, grp_iter->gr_passwd);
if (grp_iter->gr_mem != NULL) {
size_t nmem;
for (nmem = 0; grp_iter->gr_mem[nmem] != NULL; nmem++);
grp->gr_mem = talloc_zero_array(grp, char *, nmem + 1);
if (grp->gr_mem == NULL) {
ret = ENOMEM;
goto done;
}
for (nmem = 0; grp_iter->gr_mem[nmem] != NULL; nmem++) {
grp->gr_mem[nmem] = talloc_strdup(grp, grp_iter->gr_mem[nmem]);
if (grp->gr_mem[nmem] == NULL) {
ret = ENOMEM;
goto done;
}
}
}
groups[n_groups] = grp;
n_groups++;
if (n_groups % FILES_REALLOC_CHUNK == 0) {
groups = talloc_realloc(mem_ctx,
groups,
struct group *,
talloc_array_length(groups) + FILES_REALLOC_CHUNK);
if (groups == NULL) {
ret = ENOMEM;
goto done;
}
}
}
ret = EOK;
groups[n_groups] = NULL;
*_groups = groups;
done:
if (ret != EOK) {
talloc_free(groups);
}
if (grp_handle) {
close_ret = fclose(grp_handle);
if (close_ret != 0) {
close_ret = errno;
DEBUG(SSSDBG_CRIT_FAILURE,
"Cannot close group file %s [%d]\n",
id_ctx->group_file, close_ret);
}
}
return ret;
}
static errno_t delete_all_users(struct sss_domain_info *dom)
{
TALLOC_CTX *tmp_ctx;
struct ldb_dn *base_dn;
errno_t ret;
tmp_ctx = talloc_new(NULL);
if (tmp_ctx == NULL) {
DEBUG(SSSDBG_FATAL_FAILURE, "Out of memory!\n");
return ENOMEM;
}
base_dn = sysdb_user_base_dn(tmp_ctx, dom);
if (base_dn == NULL) {
DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n");
ret = ENOMEM;
goto done;
}
ret = sysdb_delete_recursive(dom->sysdb, base_dn, true);
if (ret != EOK) {
DEBUG(SSSDBG_OP_FAILURE, "Unable to delete users subtree [%d]: %s\n",
ret, sss_strerror(ret));
goto done;
}
ret = EOK;
done:
talloc_free(tmp_ctx);
return ret;
}
static errno_t save_file_user(struct files_id_ctx *id_ctx,
struct passwd *pw)
{
errno_t ret;
char *fqname;
TALLOC_CTX *tmp_ctx = NULL;
const char *shell;
const char *gecos;
struct sysdb_attrs *attrs = NULL;
if (strcmp(pw->pw_name, "root") == 0
|| pw->pw_uid == 0
|| pw->pw_gid == 0) {
DEBUG(SSSDBG_TRACE_FUNC, "Skipping %s\n", pw->pw_name);
return EOK;
}
tmp_ctx = talloc_new(NULL);
if (tmp_ctx == NULL) {
return ENOMEM;
}
fqname = sss_create_internal_fqname(tmp_ctx, pw->pw_name,
id_ctx->domain->name);
if (fqname == NULL) {
ret = ENOMEM;
goto done;
}
attrs = sysdb_new_attrs(tmp_ctx);
if (attrs == NULL) {
ret = ENOMEM;
goto done;
}
if (pw->pw_shell && pw->pw_shell[0] != '\0') {
shell = pw->pw_shell;
} else {
shell = NULL;
}
if (pw->pw_gecos && pw->pw_gecos[0] != '\0') {
gecos = pw->pw_gecos;
} else {
gecos = NULL;
}
/* FIXME - optimize later */
ret = sysdb_store_user(id_ctx->domain,
fqname,
pw->pw_passwd,
pw->pw_uid,
pw->pw_gid,
gecos,
pw->pw_dir,
shell,
NULL, attrs,
NULL, 0, 0);
if (ret != EOK) {
goto done;
}
ret = EOK;
done:
talloc_free(tmp_ctx);
return ret;
}
static errno_t sf_enum_groups(struct files_id_ctx *id_ctx);
errno_t sf_enum_users(struct files_id_ctx *id_ctx)
{
errno_t ret;
errno_t tret;
TALLOC_CTX *tmp_ctx = NULL;
struct passwd **users = NULL;
bool in_transaction = false;
tmp_ctx = talloc_new(NULL);
if (tmp_ctx == NULL) {
return ENOMEM;
}
ret = enum_files_users(tmp_ctx, id_ctx, &users);
if (ret != EOK) {
goto done;
}
ret = sysdb_transaction_start(id_ctx->domain->sysdb);
if (ret != EOK) {
goto done;
}
in_transaction = true;
/* remove previous cache contents */
/* FIXME - this is terribly inefficient */
ret = delete_all_users(id_ctx->domain);
if (ret != EOK) {
goto done;
}
for (size_t i = 0; users[i]; i++) {
ret = save_file_user(id_ctx, users[i]);
if (ret != EOK) {
DEBUG(SSSDBG_MINOR_FAILURE,
"Cannot save user %s: [%d]: %s\n",
users[i]->pw_name, ret, sss_strerror(ret));
continue;
}
}
ret = sysdb_transaction_commit(id_ctx->domain->sysdb);
if (ret != EOK) {
goto done;
}
in_transaction = false;
/* Covers the case when someone edits /etc/group, adds a group member and
* only then edits passwd and adds the user. The reverse is not needed,
* because member/memberof links are established when groups are saved.
*/
ret = sf_enum_groups(id_ctx);
if (ret != EOK) {
DEBUG(SSSDBG_OP_FAILURE, "Cannot refresh groups\n");
goto done;
}
ret = EOK;
done:
if (in_transaction) {
tret = sysdb_transaction_cancel(id_ctx->domain->sysdb);
if (tret != EOK) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Cannot cancel transaction: %d\n", ret);
}
}
talloc_free(tmp_ctx);
return ret;
}
static const char **get_cached_user_names(TALLOC_CTX *mem_ctx,
struct sss_domain_info *dom)
{
errno_t ret;
struct ldb_result *res = NULL;
const char **user_names = NULL;
unsigned c = 0;
ret = sysdb_enumpwent(mem_ctx, dom, &res);
if (ret != EOK) {
goto done;
}
user_names = talloc_zero_array(mem_ctx, const char *, res->count + 1);
if (user_names == NULL) {
goto done;
}
for (unsigned i = 0; i < res->count; i++) {
user_names[c] = ldb_msg_find_attr_as_string(res->msgs[i],
SYSDB_NAME,
NULL);
if (user_names[c] == NULL) {
continue;
}
c++;
}
done:
/* Don't free res and keep it around to avoid duplicating the names */
return user_names;
}
static errno_t delete_all_groups(struct sss_domain_info *dom)
{
TALLOC_CTX *tmp_ctx;
struct ldb_dn *base_dn;
errno_t ret;
tmp_ctx = talloc_new(NULL);
if (tmp_ctx == NULL) {
DEBUG(SSSDBG_FATAL_FAILURE, "Out of memory!\n");
return ENOMEM;
}
base_dn = sysdb_group_base_dn(tmp_ctx, dom);
if (base_dn == NULL) {
DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n");
ret = ENOMEM;
goto done;
}
ret = sysdb_delete_recursive(dom->sysdb, base_dn, true);
if (ret != EOK) {
DEBUG(SSSDBG_OP_FAILURE, "Unable to delete groups subtree [%d]: %s\n",
ret, sss_strerror(ret));
goto done;
}
ret = EOK;
done:
talloc_free(tmp_ctx);
return ret;
}
static errno_t save_file_group(struct files_id_ctx *id_ctx,
struct group *grp,
const char **cached_users)
{
errno_t ret;
char *fqname;
struct sysdb_attrs *attrs = NULL;
TALLOC_CTX *tmp_ctx = NULL;
char **fq_gr_files_mem;
const char **fq_gr_mem;
unsigned mi = 0;
if (strcmp(grp->gr_name, "root") == 0
|| grp->gr_gid == 0) {
DEBUG(SSSDBG_TRACE_FUNC, "Skipping %s\n", grp->gr_name);
return EOK;
}
tmp_ctx = talloc_new(NULL);
if (tmp_ctx == NULL) {
return ENOMEM;
}
fqname = sss_create_internal_fqname(tmp_ctx, grp->gr_name,
id_ctx->domain->name);
if (fqname == NULL) {
ret = ENOMEM;
goto done;
}
attrs = sysdb_new_attrs(tmp_ctx);
if (attrs == NULL) {
ret = ENOMEM;
goto done;
}
if (grp->gr_mem && grp->gr_mem[0]) {
fq_gr_files_mem = sss_create_internal_fqname_list(
tmp_ctx,
(const char *const*) grp->gr_mem,
id_ctx->domain->name);
if (fq_gr_files_mem == NULL) {
ret = ENOMEM;
goto done;
}
fq_gr_mem = talloc_zero_array(tmp_ctx, const char *,
talloc_array_length(fq_gr_files_mem));
if (fq_gr_mem == NULL) {
ret = ENOMEM;
goto done;
}
for (unsigned i=0; fq_gr_files_mem[i] != NULL; i++) {
if (string_in_list(fq_gr_files_mem[i],
discard_const(cached_users),
true)) {
fq_gr_mem[mi] = fq_gr_files_mem[i];
mi++;
DEBUG(SSSDBG_TRACE_LIBS,
"User %s is cached, will become a member of %s\n",
fq_gr_files_mem[i], grp->gr_name);
} else {
ret = sysdb_attrs_add_string(attrs,
SYSDB_GHOST,
fq_gr_files_mem[i]);
if (ret != EOK) {
DEBUG(SSSDBG_MINOR_FAILURE,
"Cannot add ghost %s for group %s\n",
fq_gr_files_mem[i], fqname);
continue;
}
DEBUG(SSSDBG_TRACE_LIBS,
"User %s is not cached, will become a ghost of %s\n",
fq_gr_files_mem[i], grp->gr_name);
}
}
if (fq_gr_mem != NULL && fq_gr_mem[0] != NULL) {
ret = sysdb_attrs_users_from_str_list(
attrs, SYSDB_MEMBER, id_ctx->domain->name,
(const char *const *) fq_gr_mem);
if (ret) {
DEBUG(SSSDBG_OP_FAILURE, "Could not add group members\n");
goto done;
}
}
}
ret = sysdb_store_group(id_ctx->domain, fqname, grp->gr_gid,
attrs, 0, 0);
if (ret) {
DEBUG(SSSDBG_OP_FAILURE, "Could not add group to cache\n");
goto done;
}
ret = EOK;
done:
talloc_free(tmp_ctx);
return ret;
}
static errno_t sf_enum_groups(struct files_id_ctx *id_ctx)
{
errno_t ret;
errno_t tret;
TALLOC_CTX *tmp_ctx = NULL;
struct group **groups = NULL;
bool in_transaction = false;
const char **cached_users = NULL;
tmp_ctx = talloc_new(NULL);
if (tmp_ctx == NULL) {
return ENOMEM;
}
ret = enum_files_groups(tmp_ctx, id_ctx, &groups);
if (ret != EOK) {
goto done;
}
cached_users = get_cached_user_names(tmp_ctx, id_ctx->domain);
if (cached_users == NULL) {
goto done;
}
ret = sysdb_transaction_start(id_ctx->domain->sysdb);
if (ret != EOK) {
goto done;
}
in_transaction = true;
/* remove previous cache contents */
ret = delete_all_groups(id_ctx->domain);
if (ret != EOK) {
goto done;
}
for (size_t i = 0; groups[i]; i++) {
ret = save_file_group(id_ctx, groups[i], cached_users);
if (ret != EOK) {
DEBUG(SSSDBG_MINOR_FAILURE,
"Cannot save group %s\n", groups[i]->gr_name);
continue;
}
}
ret = sysdb_transaction_commit(id_ctx->domain->sysdb);
if (ret != EOK) {
goto done;
}
in_transaction = false;
ret = EOK;
done:
if (in_transaction) {
tret = sysdb_transaction_cancel(id_ctx->domain->sysdb);
if (tret != EOK) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Cannot cancel transaction: %d\n", ret);
}
}
talloc_free(tmp_ctx);
return ret;
}
static void sf_cb_done(struct files_id_ctx *id_ctx)
{
/* Only activate a domain when both callbacks are done */
if (id_ctx->updating_passwd == false
&& id_ctx->updating_groups == false) {
dp_sbus_domain_active(id_ctx->be->provider,
id_ctx->domain);
}
}
static int sf_passwd_cb(const char *filename, uint32_t flags, void *pvt)
{
struct files_id_ctx *id_ctx;
errno_t ret;
id_ctx = talloc_get_type(pvt, struct files_id_ctx);
if (id_ctx == NULL) {
return EINVAL;
}
DEBUG(SSSDBG_TRACE_FUNC, "passwd notification\n");
if (strcmp(filename, id_ctx->passwd_file) != 0) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Wrong file, expected %s, got %s\n",
id_ctx->passwd_file, filename);
return EINVAL;
}
id_ctx->updating_passwd = true;
dp_sbus_domain_inconsistent(id_ctx->be->provider, id_ctx->domain);
dp_sbus_reset_users_ncache(id_ctx->be->provider, id_ctx->domain);
dp_sbus_reset_users_memcache(id_ctx->be->provider);
dp_sbus_reset_initgr_memcache(id_ctx->be->provider);
ret = sf_enum_users(id_ctx);
id_ctx->updating_passwd = false;
sf_cb_done(id_ctx);
files_account_info_finished(id_ctx, BE_REQ_USER, ret);
return ret;
}
static int sf_group_cb(const char *filename, uint32_t flags, void *pvt)
{
struct files_id_ctx *id_ctx;
errno_t ret;
id_ctx = talloc_get_type(pvt, struct files_id_ctx);
if (id_ctx == NULL) {
return EINVAL;
}
DEBUG(SSSDBG_TRACE_FUNC, "group notification\n");
if (strcmp(filename, id_ctx->group_file) != 0) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Wrong file, expected %s, got %s\n",
id_ctx->group_file, filename);
return EINVAL;
}
id_ctx->updating_groups = true;
dp_sbus_domain_inconsistent(id_ctx->be->provider, id_ctx->domain);
dp_sbus_reset_groups_ncache(id_ctx->be->provider, id_ctx->domain);
dp_sbus_reset_groups_memcache(id_ctx->be->provider);
dp_sbus_reset_initgr_memcache(id_ctx->be->provider);
ret = sf_enum_groups(id_ctx);
id_ctx->updating_groups = false;
sf_cb_done(id_ctx);
files_account_info_finished(id_ctx, BE_REQ_GROUP, ret);
return ret;
}
static void startup_enum_files(struct tevent_context *ev,
struct tevent_immediate *imm,
void *pvt)
{
struct files_id_ctx *id_ctx = talloc_get_type(pvt, struct files_id_ctx);
errno_t ret;
talloc_zfree(imm);
ret = sf_enum_users(id_ctx);
if (ret != EOK) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Enumerating users failed, data might be inconsistent!\n");
}
ret = sf_enum_groups(id_ctx);
if (ret != EOK) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Enumerating groups failed, data might be inconsistent!\n");
}
}
static struct snotify_ctx *sf_setup_watch(TALLOC_CTX *mem_ctx,
struct tevent_context *ev,
const char *filename,
snotify_cb_fn fn,
struct files_id_ctx *id_ctx)
{
return snotify_create(mem_ctx, ev, SNOTIFY_WATCH_DIR,
filename, NULL,
IN_DELETE_SELF | IN_CLOSE_WRITE | IN_MOVE_SELF | \
IN_CREATE | IN_MOVED_TO,
fn, id_ctx);
}
struct files_ctx *sf_init(TALLOC_CTX *mem_ctx,
struct tevent_context *ev,
const char *passwd_file,
const char *group_file,
struct files_id_ctx *id_ctx)
{
struct files_ctx *fctx;
struct tevent_immediate *imm;
fctx = talloc(mem_ctx, struct files_ctx);
if (fctx == NULL) {
return NULL;
}
fctx->pwd_watch = sf_setup_watch(fctx, ev, passwd_file,
sf_passwd_cb, id_ctx);
fctx->grp_watch = sf_setup_watch(fctx, ev, group_file,
sf_group_cb, id_ctx);
if (fctx->pwd_watch == NULL || fctx->grp_watch == NULL) {
talloc_free(fctx);
return NULL;
}
/* Enumerate users and groups on startup to process any changes when
* sssd was down. We schedule a request here to minimize the time
* we spend in the init function
*/
imm = tevent_create_immediate(id_ctx);
if (imm == NULL) {
DEBUG(SSSDBG_OP_FAILURE, "tevent_create_immediate failed.\n");
talloc_free(fctx);
return NULL;
}
tevent_schedule_immediate(imm, ev, startup_enum_files, id_ctx);
return fctx;
}