◆ C 言語つらい
◆ /var/db/sudo/lectured/[username] にファイルがあれば lecture 表示済み
◆ 消せばもっかい見れる 

前に書いた記事で sudo を最初にすると注意書きが出ると書きました

大いなる力には大いなる責任が伴うこと

こんなの

この記事にくれたコメントで どこでもこのメッセージでるんじゃないか というのがありました
fedora の GUI 環境でしか見た覚えがないのでどこでも出るのか軽く調べてみました

ソース

まずはソースでも見てみようと思います

公式サイト
https://www.sudo.ws/download.html

ソースは mercurial で管理されていて web 上でも見れるのですが github ほどみやすくないです
https://www.sudo.ws/repos/sudo

なのでソースをダウンロードしてローカルで見ることにしました


ざっとみたところ c で書かれてるようです
まあコア部分なコマンドならたいてい c か c++ ですよね

シンプルそうな機能に見えてファイルは 600 近くあって 10MB を超えています(解凍した状態)

display_lecture

とりあえず日本語のメッセージで grep してみると
sudo-1.8.19p2\plugins\sudoers\po\ja.po(397,2)  "あなたはシステム管理者から通常の講習を受けたはずです。\n"

こういうのがありました

前後はこんなの感じ
#: plugins/sudoers/check.c:252
msgid ""
"\n"
"We trust you have received the usual lecture from the local System\n"
"Administrator. It usually boils down to these three things:\n"
"\n"
"    #1) Respect the privacy of others.\n"
"    #2) Think before you type.\n"
"    #3) With great power comes great responsibility.\n"
"\n"
msgstr ""
"\n"
"あなたはシステム管理者から通常の講習を受けたはずです。\n"
"これは通常、以下の3点に要約されます:\n"
"\n"
"    #1) 他人のプライバシーを尊重すること。\n"
"    #2) タイプする前に考えること。\n"
"    #3) 大いなる力には大いなる責任が伴うこと。\n"
"\n"

ja ファイルに各メッセージの翻訳が入ってるようです

使われてる場所まで書いてくれてるので見てみると

こういう関数で使われていました

[plugins/sudoers/check.c]
/*
* Display sudo lecture (standard or custom).
* Returns true if the user was lectured, else false.
*/

static bool
display_lecture(int status)
{
    FILE *fp;
    char buf[BUFSIZ];
    ssize_t nread;
    struct sudo_conv_message msg;
    struct sudo_conv_reply repl;
    debug_decl(lecture, SUDOERS_DEBUG_AUTH)

    if (def_lecture == never ||
        (def_lecture == once && already_lectured(status)))
        debug_return_bool(false);

    memset(&msg, 0, sizeof(msg));
    memset(&repl, 0, sizeof(repl));

    if (def_lecture_file && (fp = fopen(def_lecture_file, "r")) != NULL) {
        while ((nread = fread(buf, sizeof(char), sizeof(buf) - 1, fp)) != 0) {
            buf[nread] = '\0';
            msg.msg_type = SUDO_CONV_ERROR_MSG;
            msg.msg = buf;
            sudo_conv(1, &msg, &repl, NULL);
        }
        fclose(fp);
    } else {
        msg.msg_type = SUDO_CONV_ERROR_MSG;
        msg.msg = _("\n"
            "We trust you have received the usual lecture from the local System\n"
            "Administrator. It usually boils down to these three things:\n\n"
            "    #1) Respect the privacy of others.\n"
            "    #2) Think before you type.\n"
            "    #3) With great power comes great responsibility.\n\n");
        sudo_conv(1, &msg, &repl, NULL);
    }
    debug_return_bool(true);
}

sudo_conv は grep しても関数定義してるのが見当たらなかったですが 使い方から
「def_lecture_file が定義されてたらファイルから されてなければ文字列リテラルからメッセージを表示」 ということに見えます
関数名とコメントからみてもパスワードのプロンプトは出さず単純に表示だけのようです

日本語環境だとこのリテラルが日本語のものに置き換わるのでしょう


上の方にある
    if (def_lecture == never ||
        (def_lecture == once && already_lectured(status)))
        debug_return_bool(false);

ここで lecture をしないように設定されてるか 1回だけ lecture する設定ですでに lecture 済みなら表示処理をスキップしてます

関数のコメントにあるように lecture を表示したら true してないと false が返る関数です

already_lectured(status)

すでに lecture 済みかの判断は already_lectured 関数でしてるようなのでこの関数を探します


[plugins/sudoers/timestamp.c]
/*
* Returns true if the user has already been lectured.
*/

bool
already_lectured(int unused)
{
char status_file[PATH_MAX];
struct stat sb;
int len;
debug_decl(already_lectured, SUDOERS_DEBUG_AUTH)

if (ts_secure_dir(def_lecture_status_dir, false, true)) {
len = snprintf(status_file, sizeof(status_file), "%s/%s",
def_lecture_status_dir, user_name);
if (len > 0 && (size_t)len < sizeof(status_file)) {
debug_return_bool(stat(status_file, &sb) == 0);
}
log_warningx(SLOG_SEND_MAIL, N_("lecture status path too long: %s/%s"),
def_lecture_status_dir, user_name);
}
debug_return_bool(false);
}

この(↓)変数展開した文字列がパスで
{def_lecture_status_dir}/{user_name}

ここにファイルがあれば lecture 済みってことみたいです
struct stat sb;
stat(status_file, &sb)
があってどうなってるのかと思いましたが stat 関数は普通に linux コマンドと同じファイル状態を確認する stat で結果が 0 のときは正常に情報がとれたということ (ファイルがあって読み取れる) のようです

調べてみたら C 言語って構造体名と関数名が同じになってもいいみたいですね
ややこしい


ところで呼び出し時に status を渡してましたが受け取った側での変数名は unused で使ってなかったです

check_user_interactive

already_lectured 呼び出し元に戻って さらにその呼び出し元を見てみます

[plugins/sudoers/check.c]
/*
* Returns true if the user successfully authenticates, false if not
* or -1 on fatal error.
*/

static int
check_user_interactive(int validated, int mode, struct passwd *auth_pw)
{
    struct sudo_conv_callback cb, *callback = NULL;
    struct getpass_closure closure;
    int status = TS_ERROR;
    int ret = -1;
    char *prompt;
    bool lectured;
    debug_decl(check_user_interactive, SUDOERS_DEBUG_AUTH)

    /* Setup closure for getpass_{suspend,resume} */
    closure.auth_pw = auth_pw;
    closure.cookie = NULL;
    sudo_pw_addref(closure.auth_pw);

    /* Open, lock and read time stamp file if we are using it. */
    if (!ISSET(mode, MODE_IGNORE_TICKET)) {
        /* Open time stamp file and check its status. */
        closure.cookie = timestamp_open(user_name, user_sid);
        if (timestamp_lock(closure.cookie, closure.auth_pw))
            status = timestamp_status(closure.cookie, closure.auth_pw);

        /* Construct callback for getpass function. */
        memset(&cb, 0, sizeof(cb));
        cb.version = SUDO_CONV_CALLBACK_VERSION;
        cb.closure = &closure;
        cb.on_suspend = getpass_suspend;
        cb.on_resume = getpass_resume;
        callback = &cb;
    }

    switch (status) {
    case TS_FATAL:
        /* Fatal error (usually setuid failure), unsafe to proceed. */
        goto done;

    case TS_CURRENT:
        /* Time stamp file is valid and current. */
        if (!ISSET(validated, FLAG_CHECK_USER)) {
            ret = true;
            break;
        }
        /* FALLTHROUGH */

    default:
        /* Bail out if we are non-interactive and a password is required */
        if (ISSET(mode, MODE_NONINTERACTIVE)) {
            validated |= FLAG_NON_INTERACTIVE;
            log_auth_failure(validated, 0);
            goto done;
        }

        /* XXX - should not lecture if askpass helper is being used. */
        lectured = display_lecture(status);

        /* Expand any escapes in the prompt. */
        prompt = expand_prompt(user_prompt ? user_prompt : def_passprompt,
            closure.auth_pw->pw_name);
        if (prompt == NULL)
            goto done;

        ret = verify_user(closure.auth_pw, prompt, validated, callback);
        if (ret == true && lectured)
            (void)set_lectured();        /* lecture error not fatal */
        free(prompt);
        break;
    }

    /*
     * Only update time stamp if user was validated.
     * Failure to update the time stamp is not a fatal error.
     */

    if (ret == true && ISSET(validated, VALIDATE_SUCCESS) && status != TS_ERROR)
        (void)timestamp_update(closure.cookie, closure.auth_pw);
done:
    if (closure.cookie != NULL)
        timestamp_close(closure.cookie);
    sudo_pw_delref(closure.auth_pw);

    debug_return_int(ret);
}

ちょっと 長め
display_lecture を呼び出してるのは switch で status が case にマッチせずに default になるとき

インタラクティブモードじゃない時(バッチファイルとか)でパスワード必要なら失敗になるみたい
FLAG_NON_INTERACTIVE で grep すると logging.c にこんなコードがありました
else if (ISSET(status, FLAG_NON_INTERACTIVE))
ret = log_warningx(flags, N_("a password is required"));

インタラクティブモードなら lecture を(出す場合は)表示して認証処理
認証通って lecture を表示してたら lectured 済みに設定する

と言う感じです

ということは

lecture を表示しないように設定されてなければ最初に一回は出るはずです

VM 作る度に見ていてもいいはずなのに こんなに印象に残るメッセージを見逃してるの??

lecture 済みかはファイルのありなしでわかるようなので実際にファイルを見てみようと思います

def_lecture_status_dir

ファイルのパスは
def_lecture_status_dir と user_name で決まります

user_name はそのままユーザ名でしょう
問題は def_lecture_status_dir のほう

関数内では定義されてないのでグローバル変数?
grep してみると

[plugins/sudoers/def_data.h]
#define I_LECTURE_STATUS_DIR    45
#define def_lecture_status_dir  (sudo_defs_table[I_LECTURE_STATUS_DIR].sd_un.str)

が引っかかりました

うわ なんかめんどくさそう


sudo_defs_table は
struct sudo_defs_types sudo_defs_table[] = {
{
"syslog", T_LOGFAC|T_BOOL,
N_("Syslog facility if syslog is being used for logging: %s"),
NULL,
}, {
"syslog_goodpri", T_LOGPRI|T_BOOL,
N_("Syslog priority to use when user authenticates successfully: %s"),
NULL,
}, {
    .....
と言う感じで続いてます

45番目は
{
"lecture_status_dir", T_STR|T_PATH,
N_("Path to lecture status dir: %s"),
NULL,
}

sudo_defs_types は
/*
* Structure describing compile-time and run-time options.
*/

struct sudo_defs_types {
char *name;
int type;
char *desc;
struct def_values *values;
bool (*callback)(const union sudo_defs_val *);
union sudo_defs_val sd_un;
};
union sudo_defs_val {
int flag;
int ival;
unsigned int uival;
double fval;
enum def_tuple tuple;
char *str;
mode_t mode;
struct list_members list;
};


初期値はここでは書かれてないので複雑な構造は気にしないことにします

代入箇所を探せば良さそうなので grep すると 1 件だけヒットしました

[plugins/sudoers/defaults.c]
if ((def_lecture_status_dir = strdup(_PATH_SUDO_LECTURE_DIR)) == NULL)
goto oom;

次は _PATH_SUDO_LECTURE_DIR を grep すると

[configure]
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for sudo var dir location" >&5
$as_echo_n "checking for sudo var dir location... " >&6; }
vardir="$with_vardir"
if test -z "$vardir"; then
for d in /var/db /var/lib /var/adm /usr/adm; do
if test -d "$d"; then
vardir="$d/sudo"
break
fi
done
fi
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $vardir" >&5
$as_echo "$vardir" >&6; }
cat >>confdefs.h <<EOF
#define _PATH_SUDO_LECTURE_DIR "$vardir/lectured"
EOF

/var/db

fedora で /var/db を見てみます
[user5@fedora]~% sudo tree /var/db
/var/db
└── sudo
    └── lectured
        └── user5

2 directories, 1 file

ファイルがあります


では GUI 使ってない VM でも見てみます




……

…………

root しかいなかった

重要なことに気づきました
GUI 使わないような簡単なサーバ代わりにしてる VM では通常のユーザ作らず root だけです

つまり

sudo してなかった!!


見たことないわけですねー……


とは言っても全部が全部 root のみじゃなくユーザ作ってるのもいくつかありました


見てみると lectured ファイルがあります
見てたのかなぁ

見た覚えがないので 一度ファイルを消して sudo してみました

sudouser

出てる!

けど英語!!!


日本語の
大いなる力には大いなる責任が伴うこと

ほどインパクトがないです

これなら無意識にスルーしてる可能性は高そう

まとめ

メッセージをみたくなったら
/var/db/sudo/lectured/[username]

を消そう