/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2004-2005, 2007-2025 Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pwd.h>

#include <sudoers.h>
#include <gram.h>

static int
runas_matches_pw(struct sudoers_parse_tree *parse_tree,
    const struct cmndspec *cs, const struct passwd *pw)
{
    debug_decl(runas_matches_pw, SUDOERS_DEBUG_PARSER);

    if (cs->runasuserlist != NULL)
	debug_return_int(userlist_matches(parse_tree, pw, cs->runasuserlist));

    if (cs->runasgrouplist == NULL) {
	/* No explicit runas user or group, use default. */
	if (userpw_matches(def_runas_default, pw->pw_name, pw) == ALLOW)
	    debug_return_int(ALLOW);
    }
    debug_return_int(UNSPEC);
}

/*
 * Look up the user in the sudoers parse tree for pseudo-commands like
 * list, verify and kill.
 */
static unsigned int
sudoers_lookup_pseudo(struct sudo_nss_list *snl, struct sudoers_context *ctx,
    time_t now, sudoers_lookup_callback_fn_t callback, void *cb_data,
    int pwflag)
{
    char *saved_runchroot;
    struct passwd *root_pw = NULL;
    struct sudo_nss *nss;
    struct cmndspec *cs;
    struct privilege *priv;
    struct userspec *us;
    struct defaults *def;
    int nopass, match = UNSPEC;
    unsigned int validated = 0;
    enum def_tuple pwcheck;
    debug_decl(sudoers_lookup_pseudo, SUDOERS_DEBUG_PARSER);

    pwcheck = (pwflag == -1) ? never : sudo_defs_table[pwflag].sd_un.tuple;
    nopass = (pwcheck == never || pwcheck == all) ? true : false;

    if (ctx->runas.list_pw != NULL) {
	root_pw = sudo_getpwuid(ROOT_UID);
	if (root_pw == NULL)
	    sudo_warnx(U_("unknown uid %u"), ROOT_UID);
    } else {
	SET(validated, FLAG_NO_CHECK);
    }

    /* Don't use chroot setting for pseudo-commands. */
    saved_runchroot = def_runchroot;
    def_runchroot = NULL;

    TAILQ_FOREACH(nss, snl, entries) {
	if (nss->query(ctx, nss, ctx->user.pw) == -1) {
	    /* The query function should have printed an error message. */
	    SET(validated, VALIDATE_ERROR);
	    break;
	}

	/*
	 * We have to traverse the policy forwards, not in reverse,
	 * to support the "pwcheck == all" case.
	 */
	TAILQ_FOREACH(us, &nss->parse_tree->userspecs, entries) {
	    const int user_match = userlist_matches(nss->parse_tree,
		ctx->user.pw, &us->users);
	    if (user_match != ALLOW) {
		if (callback != NULL && user_match == DENY) {
		    callback(nss->parse_tree, us, user_match, NULL, UNSPEC,
			NULL, UNSPEC, UNSPEC, UNSPEC, cb_data);
		}
		continue;
	    }
	    TAILQ_FOREACH(priv, &us->privileges, entries) {
		int priv_nopass = UNSPEC;
		const int host_match = hostlist_matches(nss->parse_tree,
		    ctx->user.pw, &priv->hostlist);
		if (host_match != ALLOW) {
		    if (callback != NULL) {
			callback(nss->parse_tree, us, user_match, priv,
			    host_match, NULL, UNSPEC, UNSPEC, UNSPEC, cb_data);
		    }
		    continue;
		}
		TAILQ_FOREACH(def, &priv->defaults, entries) {
		    if (strcmp(def->var, "authenticate") == 0) {
			priv_nopass = !def->op;
			break;
		    }
		}
		TAILQ_FOREACH(cs, &priv->cmndlist, entries) {
		    int cmnd_match = UNSPEC;
		    int date_match = UNSPEC;
		    int runas_match = UNSPEC;

		    if (cs->notbefore != UNSPEC) {
			date_match = now < cs->notbefore ? DENY : ALLOW;
		    }
		    if (cs->notafter != UNSPEC) {
			date_match = now > cs->notafter ? DENY : ALLOW;
		    }
		    /*
		     * Root can list any user's privileges.
		     * A user may always list their own privileges.
		     */
		    if (ctx->user.uid == 0 || ctx->runas.list_pw == NULL ||
			    ctx->user.uid == ctx->runas.list_pw->pw_uid) {
			cmnd_match = ALLOW;
			runas_match = ALLOW;
		    } else if (date_match != DENY) {
			/*
			 * To list another user's privileges, the runas
			 * user must match the list user or root.
			 */
			runas_match = runas_matches_pw(nss->parse_tree, cs,
			    ctx->runas.list_pw);
			switch (runas_match) {
			case DENY:
			    break;
			case ALLOW:
			    /*
			     * RunAs user matches list user.
			     * Match on command "list" or ALL.
			     */
			    cmnd_match = cmnd_matches(nss->parse_tree,
				cs->cmnd, cs->runchroot, NULL);
			    break;
			default:
			    /*
			     * RunAs user doesn't match list user.
			     * Only allow listing if the user has
			     * "sudo ALL" for root.
			     */
			    if (root_pw != NULL &&
				    runas_matches_pw(nss->parse_tree, cs,
				    root_pw) == ALLOW) {
				runas_match = ALLOW;
				cmnd_match = cmnd_matches_all(nss->parse_tree,
				    cs->cmnd, cs->runchroot, NULL);
			    }
			    break;
			}
		    }

		    /*
		     * Apply the NOPASSWD tag if the entry matched.
		     * This is relevant for "sudo -U otheruser -l".
		     */
		    if (cmnd_match == ALLOW && runas_match == ALLOW &&
			    date_match != DENY) {
			if (pwcheck == any) {
			    if (cs->tags.nopasswd == true || priv_nopass == true)
				nopass = true;
			} else if (pwcheck == all) {
			    if (cs->tags.nopasswd != true && priv_nopass != true)
				nopass = false;
			}
		    }

		    if (callback != NULL) {
			callback(nss->parse_tree, us, user_match, priv,
			    host_match, cs, date_match, runas_match,
			    cmnd_match, cb_data);
		    }
		    if (SPECIFIED(cmnd_match)) {
			/*
			 * We take the last match but must process
			 * the entire policy for pwcheck == all.
			 */
			match = cmnd_match;
		    }
		}
	    }
	}
	if (!sudo_nss_can_continue(nss, match))
	    break;
    }
    if (root_pw != NULL)
	sudo_pw_delref(root_pw);
    if (match == ALLOW || ctx->user.uid == 0) {
	/* User has an entry for this host. */
	SET(validated, VALIDATE_SUCCESS);
    } else {
	/* No entry or user is not allowed to list other users. */
	SET(validated, VALIDATE_FAILURE);
    }
    if (pwcheck == always && def_authenticate)
	SET(validated, FLAG_CHECK_USER);
    else if (nopass == true)
	def_authenticate = false;

    /* Restore original def_runchroot. */
    def_runchroot = saved_runchroot;

    debug_return_uint(validated);
}

static int
sudoers_lookup_check(struct sudo_nss *nss, struct sudoers_context *ctx,
    unsigned int *validated, struct cmnd_info *info, time_t now,
    sudoers_lookup_callback_fn_t callback, void *cb_data,
    struct cmndspec **matching_cs, struct defaults_list **defs)
{
    struct cmndspec *cs;
    struct privilege *priv;
    struct userspec *us;
    debug_decl(sudoers_lookup_check, SUDOERS_DEBUG_PARSER);

    memset(info, 0, sizeof(*info));

    TAILQ_FOREACH_REVERSE(us, &nss->parse_tree->userspecs, userspec_list, entries) {
	const int user_match = userlist_matches(nss->parse_tree, ctx->user.pw,
	    &us->users);
	if (user_match != ALLOW) {
	    if (callback != NULL && user_match == DENY) {
		callback(nss->parse_tree, us, user_match, NULL, UNSPEC, NULL,
		    UNSPEC, UNSPEC, UNSPEC, cb_data);
	    }
	    continue;
	}
	CLR(*validated, FLAG_NO_USER);
	TAILQ_FOREACH_REVERSE(priv, &us->privileges, privilege_list, entries) {
	    const int host_match = hostlist_matches(nss->parse_tree,
		ctx->user.pw, &priv->hostlist);
	    if (host_match == ALLOW) {
		CLR(*validated, FLAG_NO_HOST);
	    } else {
		if (callback != NULL) {
		    callback(nss->parse_tree, us, user_match, priv, host_match,
			NULL, UNSPEC, UNSPEC, UNSPEC, cb_data);
		}
		continue;
	    }
	    TAILQ_FOREACH_REVERSE(cs, &priv->cmndlist, cmndspec_list, entries) {
		int cmnd_match = UNSPEC;
		int date_match = UNSPEC;
		int runas_match = UNSPEC;

		if (cs->notbefore != UNSPEC) {
		    date_match = now < cs->notbefore ? DENY : ALLOW;
		}
		if (cs->notafter != UNSPEC) {
		    date_match = now > cs->notafter ? DENY : ALLOW;
		}
		if (date_match != DENY) {
		    runas_match = runaslist_matches(nss->parse_tree,
			cs->runasuserlist, cs->runasgrouplist);
		    if (runas_match == ALLOW) {
			cmnd_match = cmnd_matches(nss->parse_tree, cs->cmnd,
			    cs->runchroot, info);
		    }
		}
		if (callback != NULL) {
		    callback(nss->parse_tree, us, user_match, priv, host_match,
			cs, date_match, runas_match, cmnd_match, cb_data);
		}

		if (SPECIFIED(cmnd_match)) {
		    *matching_cs = cs;
		    *defs = &priv->defaults;
		    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
			"userspec matched @ %s:%d:%d: %s",
			us->file ? us->file : "???", us->line, us->column,
			cmnd_match ? "allowed" : "denied");
		    debug_return_int(cmnd_match);
		}
		free(info->cmnd_path);
		memset(info, 0, sizeof(*info));
	    }
	}
    }
    debug_return_int(UNSPEC);
}

/*
 * Apply cmndspec-specific settings including SELinux role/type,
 * AppArmor profile, Solaris privs, and command tags.
 */
static bool
apply_cmndspec(struct sudoers_context *ctx, struct cmndspec *cs)
{
    debug_decl(apply_cmndspec, SUDOERS_DEBUG_PARSER);

    if (cs != NULL) {
	/* Set role and type if not specified on command line. */
	if (ctx->runas.role == NULL) {
	    if (cs->role != NULL) {
		ctx->runas.role = strdup(cs->role);
		if (ctx->runas.role == NULL) {
		    sudo_warnx(U_("%s: %s"), __func__,
			U_("unable to allocate memory"));
		    debug_return_bool(false);
		}
	    } else {
		ctx->runas.role = def_role;
		def_role = NULL;
	    }
	    if (ctx->runas.role != NULL) {
		sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		    "ctx->runas.role -> %s", ctx->runas.role);
	    }
	}
	if (ctx->runas.type == NULL) {
	    if (cs->type != NULL) {
		ctx->runas.type = strdup(cs->type);
		if (ctx->runas.type == NULL) {
		    sudo_warnx(U_("%s: %s"), __func__,
			U_("unable to allocate memory"));
		    debug_return_bool(false);
		}
	    } else {
		ctx->runas.type = def_type;
		def_type = NULL;
	    }
	    if (ctx->runas.type != NULL) {
		sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		    "ctx->runas.type -> %s", ctx->runas.type);
	    }
	}
	/* Set AppArmor profile, if specified */
	if (cs->apparmor_profile != NULL) {
	    free(ctx->runas.apparmor_profile);
	    ctx->runas.apparmor_profile = strdup(cs->apparmor_profile);
	    if (ctx->runas.apparmor_profile == NULL) {
		sudo_warnx(U_("%s: %s"), __func__,
		    U_("unable to allocate memory"));
		debug_return_bool(false);
	    }
	} else {
	    free(ctx->runas.apparmor_profile);
	    ctx->runas.apparmor_profile = def_apparmor_profile;
	    def_apparmor_profile = NULL;
	}
	if (ctx->runas.apparmor_profile != NULL) {
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"ctx->runas.apparmor_profile -> %s", ctx->runas.apparmor_profile);
	}
	/* Set Solaris privilege sets */
	if (cs->privs != NULL) {
	    free(ctx->runas.privs);
	    ctx->runas.privs = strdup(cs->privs);
	    if (ctx->runas.privs == NULL) {
		sudo_warnx(U_("%s: %s"), __func__,
		    U_("unable to allocate memory"));
		debug_return_bool(false);
	    }
	} else {
	    free(ctx->runas.privs);
	    ctx->runas.privs = def_privs;
	    def_privs = NULL;
	}
	if (ctx->runas.privs != NULL) {
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"ctx->runas.privs -> %s", ctx->runas.privs);
	}
	if (cs->limitprivs != NULL) {
	    free(ctx->runas.limitprivs);
	    ctx->runas.limitprivs = strdup(cs->limitprivs);
	    if (ctx->runas.limitprivs == NULL) {
		sudo_warnx(U_("%s: %s"), __func__,
		    U_("unable to allocate memory"));
		debug_return_bool(false);
	    }
	} else {
	    free(ctx->runas.limitprivs);
	    ctx->runas.limitprivs = def_limitprivs;
	    def_limitprivs = NULL;
	}
	if (ctx->runas.limitprivs != NULL) {
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"ctx->runas.limitprivs -> %s", ctx->runas.limitprivs);
	}
	if (cs->timeout > 0) {
	    def_command_timeout = cs->timeout;
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"def_command_timeout -> %d", def_command_timeout);
	}
	if (cs->runcwd != NULL) {
	    free(def_runcwd);
	    def_runcwd = strdup(cs->runcwd);
	    if (def_runcwd == NULL) {
		sudo_warnx(U_("%s: %s"), __func__,
		    U_("unable to allocate memory"));
		debug_return_bool(false);
	    }
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"def_runcwd -> %s", def_runcwd);
	}
	if (cs->runchroot != NULL) {
	    free(def_runchroot);
	    def_runchroot = strdup(cs->runchroot);
	    if (def_runchroot == NULL) {
		sudo_warnx(U_("%s: %s"), __func__,
		    U_("unable to allocate memory"));
		debug_return_bool(false);
	    }
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"def_runchroot -> %s", def_runchroot);
	}
	if (cs->tags.nopasswd != UNSPEC) {
	    def_authenticate = !cs->tags.nopasswd;
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"def_authenticate -> %s", def_authenticate ? "true" : "false");
	}
	if (cs->tags.noexec != UNSPEC) {
	    def_noexec = cs->tags.noexec;
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"def_noexec -> %s", def_noexec ? "true" : "false");
	}
	if (cs->tags.intercept != UNSPEC) {
	    def_intercept = cs->tags.intercept;
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"def_intercept -> %s", def_intercept ? "true" : "false");
	}
	if (cs->tags.setenv != UNSPEC) {
	    def_setenv = cs->tags.setenv;
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"def_setenv -> %s", def_setenv ? "true" : "false");
	}
	if (cs->tags.log_input != UNSPEC) {
	    def_log_input = cs->tags.log_input;
	    cb_log_input(ctx, NULL, 0, 0, NULL, cs->tags.log_input);
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"def_log_input -> %s", def_log_input ? "true" : "false");
	}
	if (cs->tags.log_output != UNSPEC) {
	    def_log_output = cs->tags.log_output;
	    cb_log_output(ctx, NULL, 0, 0, NULL, cs->tags.log_output);
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"def_log_output -> %s", def_log_output ? "true" : "false");
	}
	if (cs->tags.send_mail != UNSPEC) {
	    if (cs->tags.send_mail) {
		def_mail_all_cmnds = true;
		sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		    "def_mail_all_cmnds -> true");
	    } else {
		def_mail_all_cmnds = false;
		def_mail_always = false;
		def_mail_no_perms = false;
		sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		    "def_mail_all_cmnds -> false, def_mail_always -> false, "
		    "def_mail_no_perms -> false");
	    }
	}
	if (cs->tags.follow != UNSPEC) {
	    def_sudoedit_follow = cs->tags.follow;
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"def_sudoedit_follow -> %s", def_sudoedit_follow ? "true" : "false");
	}
    }

    debug_return_bool(true);
}

/*
 * Look up the user in the sudoers parse tree and check to see if they are
 * allowed to run the specified command on this host as the target user.
 */
unsigned int
sudoers_lookup(struct sudo_nss_list *snl, struct sudoers_context *ctx,
    time_t now, sudoers_lookup_callback_fn_t callback, void *cb_data,
    int *cmnd_status, int pwflag)
{
    struct defaults_list *defs = NULL;
    struct sudoers_parse_tree *parse_tree = NULL;
    struct cmndspec *cs = NULL;
    struct sudo_nss *nss;
    struct cmnd_info info;
    unsigned int validated = FLAG_NO_USER | FLAG_NO_HOST;
    int m, match = UNSPEC;
    debug_decl(sudoers_lookup, SUDOERS_DEBUG_PARSER);

    /*
     * Special case checking the "validate", "list" and "kill" pseudo-commands.
     */
    if (pwflag) {
	debug_return_uint(sudoers_lookup_pseudo(snl, ctx, now, callback,
	    cb_data, pwflag));
    }

    /* Need to be runas user while stat'ing things. */
    if (!set_perms(ctx, PERM_RUNAS))
	debug_return_uint(validated);

    /* Query each sudoers source and check the user. */
    TAILQ_FOREACH(nss, snl, entries) {
	if (nss->query(ctx, nss, ctx->user.pw) == -1) {
	    /* The query function should have printed an error message. */
	    SET(validated, VALIDATE_ERROR);
	    break;
	}

	m = sudoers_lookup_check(nss, ctx, &validated, &info, now, callback,
	    cb_data, &cs, &defs);
	if (SPECIFIED(m)) {
	    match = m;
	    parse_tree = nss->parse_tree;
	}

	if (!sudo_nss_can_continue(nss, m))
	    break;
    }
    if (SPECIFIED(match)) {
	if (info.cmnd_path != NULL) {
	    /* Update cmnd, cmnd_stat, cmnd_status from matching entry. */
	    free(ctx->user.cmnd);
	    ctx->user.cmnd = info.cmnd_path;
	    if (ctx->user.cmnd_stat != NULL)
		*ctx->user.cmnd_stat = info.cmnd_stat;
	    *cmnd_status = info.status;
	}
	if (defs != NULL)
	    (void)update_defaults(ctx, parse_tree, defs, SETDEF_GENERIC, false);
	if (!apply_cmndspec(ctx, cs))
	    SET(validated, VALIDATE_ERROR);
	else if (match == ALLOW)
	    SET(validated, VALIDATE_SUCCESS);
	else
	    SET(validated, VALIDATE_FAILURE);
    }
    if (!restore_perms())
	SET(validated, VALIDATE_ERROR);
    debug_return_uint(validated);
}
