/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <gshadow.h>
#include <string.h>

#include "env-util.h"
#include "nss-systemd.h"
#include "strv.h"
#include "time-util.h"
#include "user-record.h"
#include "user-record-nss.h"
#include "user-util.h"
#include "userdb.h"
#include "userdb-glue.h"

UserDBFlags nss_glue_userdb_flags(void) {
        UserDBFlags flags = USERDB_EXCLUDE_NSS;

        /* Make sure that we don't go in circles when allocating a dynamic UID by checking our own database */
        if (secure_getenv_bool("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
                flags |= USERDB_EXCLUDE_DYNAMIC_USER;

        return flags;
}

int nss_pack_user_record(
                UserRecord *hr,
                struct passwd *pwd,
                char *buffer,
                size_t buflen) {

        const char *rn, *hd, *shell;
        size_t required;

        assert(hr);
        assert(pwd);

        assert(hr->user_name);
        required = strlen(hr->user_name) + 1;

        required += 2; /* strlen(PASSWORD_SEE_SHADOW) + 1 */

        assert_se(rn = user_record_real_name(hr));
        required += strlen(rn) + 1;

        assert_se(hd = user_record_home_directory(hr));
        required += strlen(hd) + 1;

        assert_se(shell = user_record_shell(hr));
        required += strlen(shell) + 1;

        if (buflen < required)
                return -ERANGE;

        *pwd = (struct passwd) {
                .pw_name = buffer,
                .pw_uid = hr->uid,
                .pw_gid = user_record_gid(hr),
        };

        assert(buffer);

        pwd->pw_passwd = stpcpy(pwd->pw_name, hr->user_name) + 1;
        pwd->pw_gecos = stpcpy(pwd->pw_passwd, PASSWORD_SEE_SHADOW) + 1;
        pwd->pw_dir = stpcpy(pwd->pw_gecos, rn) + 1;
        pwd->pw_shell = stpcpy(pwd->pw_dir, hd) + 1;
        strcpy(pwd->pw_shell, shell);

        return 0;
}

enum nss_status userdb_getpwnam(
                const char *name,
                struct passwd *pwd,
                char *buffer, size_t buflen,
                int *errnop) {

        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
        int r;

        assert(pwd);
        assert(errnop);

        if (_nss_systemd_is_blocked())
                return NSS_STATUS_NOTFOUND;

        r = userdb_by_name(name, /* match= */ NULL, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &hr);
        if (r == -ESRCH)
                return NSS_STATUS_NOTFOUND;
        if (r < 0) {
                *errnop = -r;
                return NSS_STATUS_UNAVAIL;
        }

        r = nss_pack_user_record(hr, pwd, buffer, buflen);
        if (r < 0) {
                *errnop = -r;
                return NSS_STATUS_TRYAGAIN;
        }

        return NSS_STATUS_SUCCESS;
}

enum nss_status userdb_getpwuid(
                uid_t uid,
                struct passwd *pwd,
                char *buffer,
                size_t buflen,
                int *errnop) {

        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
        int r;

        assert(pwd);
        assert(errnop);

        if (_nss_systemd_is_blocked())
                return NSS_STATUS_NOTFOUND;

        r = userdb_by_uid(uid, /* match= */ NULL, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &hr);
        if (r == -ESRCH)
                return NSS_STATUS_NOTFOUND;
        if (r < 0) {
                *errnop = -r;
                return NSS_STATUS_UNAVAIL;
        }

        r = nss_pack_user_record(hr, pwd, buffer, buflen);
        if (r < 0) {
                *errnop = -r;
                return NSS_STATUS_TRYAGAIN;
        }

        return NSS_STATUS_SUCCESS;
}

int nss_pack_user_record_shadow(
                UserRecord *hr,
                struct spwd *spwd,
                char *buffer,
                size_t buflen) {

        const char *hashed;
        size_t required;

        assert(hr);
        assert(spwd);

        assert(hr->user_name);
        required = strlen(hr->user_name) + 1;

        assert_se(hashed = strv_isempty(hr->hashed_password) ? PASSWORD_LOCKED_AND_INVALID : hr->hashed_password[0]);
        required += strlen(hashed) + 1;

        if (buflen < required)
                return -ERANGE;

        *spwd = (struct spwd) {
                .sp_namp = buffer,
                .sp_lstchg = hr->last_password_change_usec == 0 ? 1 :               /* map 0 to 1, since 0 means please change password on next login */
                             hr->last_password_change_usec == UINT64_MAX ? -1 :
                             (long) (hr->last_password_change_usec / USEC_PER_DAY),
                .sp_min = hr->password_change_min_usec != UINT64_MAX ? (long) (hr->password_change_min_usec / USEC_PER_DAY) : -1,
                .sp_max = hr->password_change_max_usec != UINT64_MAX ? (long) (hr->password_change_max_usec / USEC_PER_DAY) : -1,
                .sp_warn = hr->password_change_warn_usec != UINT64_MAX ? (long) (hr->password_change_warn_usec / USEC_PER_DAY) : -1,
                .sp_inact = hr->password_change_inactive_usec != UINT64_MAX ? (long) (hr->password_change_inactive_usec / USEC_PER_DAY) : -1,
                .sp_expire = hr->locked > 0 || hr->not_after_usec == 0 ? 1 : /* already expired/locked */
                             hr->not_after_usec == UINT64_MAX ? -1 :
                             (long) (hr->not_after_usec / USEC_PER_DAY),
                .sp_flag = ULONG_MAX,
        };

        assert(buffer);

        spwd->sp_pwdp = stpcpy(spwd->sp_namp, hr->user_name) + 1;
        strcpy(spwd->sp_pwdp, hashed);

        return 0;
}

enum nss_status userdb_getspnam(
                const char *name,
                struct spwd *spwd,
                char *buffer, size_t buflen,
                int *errnop) {

        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
        int r;

        assert(spwd);
        assert(errnop);

        if (_nss_systemd_is_blocked())
                return NSS_STATUS_NOTFOUND;

        r = userdb_by_name(name, /* match= */ NULL, nss_glue_userdb_flags(), &hr);
        if (r == -ESRCH)
                return NSS_STATUS_NOTFOUND;
        if (r < 0) {
                *errnop = -r;
                return NSS_STATUS_UNAVAIL;
        }

        if (hr->incomplete) /* protected records missing? */
                return NSS_STATUS_NOTFOUND;

        r = nss_pack_user_record_shadow(hr, spwd, buffer, buflen);
        if (r < 0) {
                *errnop = -r;
                return NSS_STATUS_TRYAGAIN;
        }

        return NSS_STATUS_SUCCESS;
}

int nss_pack_group_record(
                GroupRecord *g,
                char **extra_members,
                struct group *gr,
                char *buffer,
                size_t buflen) {

        char **array = NULL, *p;
        size_t required, n = 0, i = 0;

        assert(g);
        assert(gr);

        assert(g->group_name);
        required = strlen(g->group_name) + 1;

        STRV_FOREACH(m, g->members) {
                required += sizeof(char*);  /* space for ptr array entry */
                required += strlen(*m) + 1;
                n++;
        }
        STRV_FOREACH(m, extra_members) {
                if (strv_contains(g->members, *m))
                        continue;

                required += sizeof(char*);
                required += strlen(*m) + 1;
                n++;
        }

        required += sizeof(char*); /* trailing NULL in ptr array entry */

        if (buflen < required)
                return -ERANGE;

        array = (char**) buffer; /* place ptr array at beginning of buffer, under assumption buffer is aligned */
        p = buffer + sizeof(void*) * (n + 1); /* place member strings right after the ptr array */

        STRV_FOREACH(m, g->members) {
                array[i++] = p;
                p = stpcpy(p, *m) + 1;
        }
        STRV_FOREACH(m, extra_members) {
                if (strv_contains(g->members, *m))
                        continue;

                array[i++] = p;
                p = stpcpy(p, *m) + 1;
        }

        assert_se(i == n);
        array[n] = NULL;

        *gr = (struct group) {
                .gr_name = strcpy(p, g->group_name),
                .gr_gid = g->gid,
                .gr_passwd = (char*) PASSWORD_SEE_SHADOW,
                .gr_mem = array,
        };

        return 0;
}

enum nss_status userdb_getgrnam(
                const char *name,
                struct group *gr,
                char *buffer,
                size_t buflen,
                int *errnop) {

        _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
        _cleanup_strv_free_ char **members = NULL;
        int r;

        assert(gr);
        assert(errnop);

        if (_nss_systemd_is_blocked())
                return NSS_STATUS_NOTFOUND;

        r = groupdb_by_name(name, /* match= */ NULL, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &g);
        if (r < 0 && r != -ESRCH) {
                *errnop = -r;
                return NSS_STATUS_UNAVAIL;
        }

        r = membershipdb_by_group_strv(name, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &members);
        if (r < 0 && r != -ESRCH) {
                *errnop = -r;
                return NSS_STATUS_UNAVAIL;
        }

        if (!g) {
                _unused_ _cleanup_(_nss_systemd_unblockp) bool blocked = false;

                if (strv_isempty(members))
                        return NSS_STATUS_NOTFOUND;

                /* Grmbl, so we are supposed to extend a group entry, but the group entry itself is not
                 * accessible via non-NSS. Hence let's do what we have to do, and query NSS after all to
                 * acquire it, so that we can extend it (that's because glibc's group merging feature will
                 * merge groups only if both GID and name match and thus we need to have both first). It
                 * sucks behaving recursively likely this, but it's apparently what everybody does. We break
                 * the recursion for ourselves via the _nss_systemd_block_nss() lock. */

                r = _nss_systemd_block(true);
                if (r < 0)
                        return r;

                blocked = true;

                r = nss_group_record_by_name(name, false, &g);
                if (r == -ESRCH)
                        return NSS_STATUS_NOTFOUND;
                if (r < 0) {
                        *errnop = -r;
                        return NSS_STATUS_UNAVAIL;
                }
        }

        r = nss_pack_group_record(g, members, gr, buffer, buflen);
        if (r < 0) {
                *errnop = -r;
                return NSS_STATUS_TRYAGAIN;
        }

        return NSS_STATUS_SUCCESS;
}

enum nss_status userdb_getgrgid(
                gid_t gid,
                struct group *gr,
                char *buffer,
                size_t buflen,
                int *errnop) {

        _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
        _cleanup_strv_free_ char **members = NULL;
        bool from_nss;
        int r;

        assert(gr);
        assert(errnop);

        if (_nss_systemd_is_blocked())
                return NSS_STATUS_NOTFOUND;

        r = groupdb_by_gid(gid, /* match= */ NULL, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &g);
        if (r < 0 && r != -ESRCH) {
                *errnop = -r;
                return NSS_STATUS_UNAVAIL;
        }

        if (!g) {
                _unused_ _cleanup_(_nss_systemd_unblockp) bool blocked = false;

                /* So, quite possibly we have to extend an existing group record with additional members. But
                 * to do this we need to know the group name first. The group didn't exist via non-NSS
                 * queries though, hence let's try to acquire it here recursively via NSS. */

                r = _nss_systemd_block(true);
                if (r < 0)
                        return r;

                blocked = true;

                r = nss_group_record_by_gid(gid, false, &g);
                if (r == -ESRCH)
                        return NSS_STATUS_NOTFOUND;
                if (r < 0) {
                        *errnop = -r;
                        return NSS_STATUS_UNAVAIL;
                }

                from_nss = true;
        } else
                from_nss = false;

        r = membershipdb_by_group_strv(g->group_name, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &members);
        if (r < 0 && r != -ESRCH) {
                *errnop = -r;
                return NSS_STATUS_UNAVAIL;
        }

        /* If we acquired the record via NSS then there's no reason to respond unless we have to augment the
         * list of members of the group */
        if (from_nss && strv_isempty(members))
                return NSS_STATUS_NOTFOUND;

        r = nss_pack_group_record(g, members, gr, buffer, buflen);
        if (r < 0) {
                *errnop = -r;
                return NSS_STATUS_TRYAGAIN;
        }

        return NSS_STATUS_SUCCESS;
}

/* Counts string pointers (including terminating NULL element) of given
 * string vector strv and stores amount of pointers in n and total
 * length of all contained strings including NUL bytes in len. */
static void nss_count_strv(char * const *strv, size_t *n, size_t *len) {
        STRV_FOREACH(str, strv) {
                (*len) += sizeof(char*);  /* space for array entry */
                (*len) += strlen(*str) + 1;
                (*n)++;
        }
        (*len) += sizeof(char*); /* trailing NULL in array entry */
        (*n)++;
}

/* Performs deep copy of given string vector src and stores content
 * of contained strings into buf with references to these strings
 * in dst. At dst location, a new NULL-terminated string vector is
 * created. The dst and buf locations are updated to point just behind
 * the last pointer or char respectively. Returns total amount of
 * pointers in newly created string vector in dst, including the
 * terminating NULL element. */
static size_t nss_deep_copy_strv(char * const *src, char ***dst, char **buf) {
        char *p = *buf;
        size_t i = 0;

        STRV_FOREACH(str, src) {
                (*dst)[i++] = p;
                p = stpcpy(p, *str) + 1;
        }
        (*dst)[i++] = NULL;
        *dst += i;
        *buf = p;
        return i;
}

int nss_pack_group_record_shadow(
                GroupRecord *hr,
                struct sgrp *sgrp,
                char *buffer,
                size_t buflen) {

        const char *hashed;
        char **array = NULL, *p;
        size_t i = 0, n = 0, required;

        assert(hr);
        assert(sgrp);

        assert(hr->group_name);
        required = strlen(hr->group_name) + 1;

        assert_se(hashed = strv_isempty(hr->hashed_password) ? PASSWORD_LOCKED_AND_INVALID : hr->hashed_password[0]);
        required += strlen(hashed) + 1;

        nss_count_strv(hr->administrators, &n, &required);
        nss_count_strv(hr->members, &n, &required);

        if (buflen < required)
                return -ERANGE;

        assert(buffer);

        p = buffer + sizeof(void*) * (n + 1); /* place member strings right after the ptr array */
        array = (char**) buffer; /* place ptr array at beginning of buffer, under assumption buffer is aligned */

        sgrp->sg_mem = array;
        i += nss_deep_copy_strv(hr->members, &array, &p);

        sgrp->sg_adm = array;
        i += nss_deep_copy_strv(hr->administrators, &array, &p);

        assert_se(i == n);

        sgrp->sg_namp = p;
        sgrp->sg_passwd = stpcpy(sgrp->sg_namp, hr->group_name) + 1;
        strcpy(sgrp->sg_passwd, hashed);

        return 0;
}

enum nss_status userdb_getsgnam(
                const char *name,
                struct sgrp *sgrp,
                char *buffer, size_t buflen,
                int *errnop) {

        _cleanup_(group_record_unrefp) GroupRecord *hr = NULL;
        int r;

        assert(sgrp);
        assert(errnop);

        if (_nss_systemd_is_blocked())
                return NSS_STATUS_NOTFOUND;

        r = groupdb_by_name(name, /* match= */ NULL, nss_glue_userdb_flags(), &hr);
        if (r == -ESRCH)
                return NSS_STATUS_NOTFOUND;
        if (r < 0) {
                *errnop = -r;
                return NSS_STATUS_UNAVAIL;
        }

        if (hr->incomplete) /* protected records missing? */
                return NSS_STATUS_NOTFOUND;

        r = nss_pack_group_record_shadow(hr, sgrp, buffer, buflen);
        if (r < 0) {
                *errnop = -r;
                return NSS_STATUS_TRYAGAIN;
        }

        return NSS_STATUS_SUCCESS;
}
