xref: /freebsd/contrib/pam-krb5/tests/fakepam/script.c (revision bf6873c5786e333d679a7838d28812febf479a8a)
1 /*
2  * Run a PAM interaction script for testing.
3  *
4  * Provides an interface that loads a PAM interaction script from a file and
5  * runs through that script, calling the internal PAM module functions and
6  * checking their results.  This allows automation of PAM testing through
7  * external data files instead of coding everything in C.
8  *
9  * The canonical version of this file is maintained in the rra-c-util package,
10  * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
11  *
12  * Written by Russ Allbery <eagle@eyrie.org>
13  * Copyright 2016, 2018, 2020-2021 Russ Allbery <eagle@eyrie.org>
14  * Copyright 2011-2012, 2014
15  *     The Board of Trustees of the Leland Stanford Junior University
16  *
17  * Permission is hereby granted, free of charge, to any person obtaining a
18  * copy of this software and associated documentation files (the "Software"),
19  * to deal in the Software without restriction, including without limitation
20  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
21  * and/or sell copies of the Software, and to permit persons to whom the
22  * Software is furnished to do so, subject to the following conditions:
23  *
24  * The above copyright notice and this permission notice shall be included in
25  * all copies or substantial portions of the Software.
26  *
27  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
30  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
33  * DEALINGS IN THE SOFTWARE.
34  *
35  * SPDX-License-Identifier: MIT
36  */
37 
38 #include <config.h>
39 #include <portable/pam.h>
40 #include <portable/system.h>
41 
42 #include <ctype.h>
43 #include <dirent.h>
44 #include <errno.h>
45 #ifdef HAVE_REGCOMP
46 #    include <regex.h>
47 #endif
48 #include <syslog.h>
49 
50 #include <tests/fakepam/internal.h>
51 #include <tests/fakepam/pam.h>
52 #include <tests/fakepam/script.h>
53 #include <tests/tap/basic.h>
54 #include <tests/tap/macros.h>
55 #include <tests/tap/string.h>
56 
57 
58 /*
59  * Compare a regex to a string.  If regular expression support isn't
60  * available, we skip this test.
61  */
62 #ifdef HAVE_REGCOMP
63 static void __attribute__((__format__(printf, 3, 4)))
like(const char * wanted,const char * seen,const char * format,...)64 like(const char *wanted, const char *seen, const char *format, ...)
65 {
66     va_list args;
67     regex_t regex;
68     char err[BUFSIZ];
69     int status;
70 
71     if (seen == NULL) {
72         fflush(stderr);
73         printf("# wanted: /%s/\n#   seen: (null)\n", wanted);
74         va_start(args, format);
75         okv(0, format, args);
76         va_end(args);
77         return;
78     }
79     memset(&regex, 0, sizeof(regex));
80     status = regcomp(&regex, wanted, REG_EXTENDED | REG_NOSUB);
81     if (status != 0) {
82         regerror(status, &regex, err, sizeof(err));
83         bail("invalid regex /%s/: %s", wanted, err);
84     }
85     status = regexec(&regex, seen, 0, NULL, 0);
86     switch (status) {
87     case 0:
88         va_start(args, format);
89         okv(1, format, args);
90         va_end(args);
91         break;
92     case REG_NOMATCH:
93         printf("# wanted: /%s/\n#   seen: %s\n", wanted, seen);
94         va_start(args, format);
95         okv(0, format, args);
96         va_end(args);
97         break;
98     default:
99         regerror(status, &regex, err, sizeof(err));
100         bail("regexec failed for regex /%s/: %s", wanted, err);
101     }
102     regfree(&regex);
103 }
104 #else  /* !HAVE_REGCOMP */
105 static void
like(const char * wanted,const char * seen,const char * format UNUSED,...)106 like(const char *wanted, const char *seen, const char *format UNUSED, ...)
107 {
108     diag("wanted /%s/", wanted);
109     diag("  seen %s", seen);
110     skip("regex support not available");
111 }
112 #endif /* !HAVE_REGCOMP */
113 
114 
115 /*
116  * Compare an expected string with a seen string, used by both output checking
117  * and prompt checking.  This is a separate function because the expected
118  * string may be a regex, determined by seeing if it starts and ends with a
119  * slash (/), which may require a regex comparison.
120  *
121  * Eventually calls either is_string or ok to report results via TAP.
122  */
123 static void __attribute__((__format__(printf, 3, 4)))
compare_string(char * wanted,char * seen,const char * format,...)124 compare_string(char *wanted, char *seen, const char *format, ...)
125 {
126     va_list args;
127     char *comment, *regex;
128     size_t length;
129 
130     /* Format the comment since we need it regardless. */
131     va_start(args, format);
132     bvasprintf(&comment, format, args);
133     va_end(args);
134 
135     /* Check whether the wanted string is a regex. */
136     length = strlen(wanted);
137     if (wanted[0] == '/' && wanted[length - 1] == '/') {
138         regex = bstrndup(wanted + 1, length - 2);
139         like(regex, seen, "%s", comment);
140         free(regex);
141     } else {
142         is_string(wanted, seen, "%s", comment);
143     }
144     free(comment);
145 }
146 
147 
148 /*
149  * The PAM conversation function.  Takes the prompts struct from the
150  * configuration and interacts appropriately.  If a prompt is of the expected
151  * type but not the expected string, it still responds; if it's not of the
152  * expected type, it returns PAM_CONV_ERR.
153  *
154  * Currently only handles a single prompt at a time.
155  */
156 static int
converse(int num_msg,const struct pam_message ** msg,struct pam_response ** resp,void * appdata_ptr)157 converse(int num_msg, const struct pam_message **msg,
158          struct pam_response **resp, void *appdata_ptr)
159 {
160     struct prompts *prompts = appdata_ptr;
161     struct prompt *prompt;
162     char *message;
163     size_t length;
164     int i;
165 
166     *resp = bcalloc(num_msg, sizeof(struct pam_response));
167     for (i = 0; i < num_msg; i++) {
168         message = bstrdup(msg[i]->msg);
169 
170         /* Remove newlines for comparison purposes. */
171         length = strlen(message);
172         while (length > 0 && message[length - 1] == '\n')
173             message[length-- - 1] = '\0';
174 
175         /* Check if we've gotten too many prompts but quietly ignore them. */
176         if (prompts->current >= prompts->size) {
177             diag("unexpected prompt: %s", message);
178             free(message);
179             ok(0, "more prompts than expected");
180             continue;
181         }
182 
183         /* Be sure everything matches and return the response, if any. */
184         prompt = &prompts->prompts[prompts->current];
185         is_int(prompt->style, msg[i]->msg_style, "style of prompt %lu",
186                (unsigned long) prompts->current + 1);
187         compare_string(prompt->prompt, message, "value of prompt %lu",
188                        (unsigned long) prompts->current + 1);
189         free(message);
190         prompts->current++;
191         if (prompt->style == msg[i]->msg_style && prompt->response != NULL) {
192             (*resp)[i].resp = bstrdup(prompt->response);
193             (*resp)[i].resp_retcode = 0;
194         }
195     }
196 
197     /*
198      * Always return success even if the prompts don't match.  Otherwise,
199      * we're likely to abort the conversation in the middle and possibly
200      * leave passwords set incorrectly.
201      */
202     return PAM_SUCCESS;
203 }
204 
205 
206 /*
207  * Check the actual PAM output against the expected output.  We divide the
208  * expected and seen output into separate lines and compare each one so that
209  * we can handle regular expressions and the output priority.
210  */
211 static void
check_output(const struct output * wanted,const struct output * seen)212 check_output(const struct output *wanted, const struct output *seen)
213 {
214     size_t i;
215 
216     if (wanted == NULL && seen == NULL)
217         ok(1, "no output");
218     else if (wanted == NULL) {
219         for (i = 0; i < seen->count; i++)
220             diag("unexpected: (%d) %s", seen->lines[i].priority,
221                  seen->lines[i].line);
222         ok(0, "no output");
223     } else if (seen == NULL) {
224         for (i = 0; i < wanted->count; i++) {
225             is_int(wanted->lines[i].priority, 0, "output priority %lu",
226                    (unsigned long) i + 1);
227             is_string(wanted->lines[i].line, NULL, "output line %lu",
228                       (unsigned long) i + 1);
229         }
230     } else {
231         for (i = 0; i < wanted->count && i < seen->count; i++) {
232             is_int(wanted->lines[i].priority, seen->lines[i].priority,
233                    "output priority %lu", (unsigned long) i + 1);
234             compare_string(wanted->lines[i].line, seen->lines[i].line,
235                            "output line %lu", (unsigned long) i + 1);
236         }
237         if (wanted->count > seen->count)
238             for (i = seen->count; i < wanted->count; i++) {
239                 is_int(wanted->lines[i].priority, 0, "output priority %lu",
240                        (unsigned long) i + 1);
241                 is_string(wanted->lines[i].line, NULL, "output line %lu",
242                           (unsigned long) i + 1);
243             }
244         if (seen->count > wanted->count) {
245             for (i = wanted->count; i < seen->count; i++)
246                 diag("unexpected: (%d) %s", seen->lines[i].priority,
247                      seen->lines[i].line);
248             ok(0, "unexpected output lines");
249         } else {
250             ok(1, "no excess output");
251         }
252     }
253 }
254 
255 
256 /*
257  * The core of the work.  Given the path to a PAM interaction script, which
258  * may be relative to C_TAP_SOURCE or C_TAP_BUILD, the user (may be NULL), and
259  * the stored password (may be NULL), run that script, outputting the results
260  * in TAP format.
261  */
262 void
run_script(const char * file,const struct script_config * config)263 run_script(const char *file, const struct script_config *config)
264 {
265     char *path;
266     struct output *output;
267     FILE *script;
268     struct work *work;
269     struct options *opts;
270     struct action *action, *oaction;
271     struct pam_conv conv = {NULL, NULL};
272     pam_handle_t *pamh;
273     int status;
274     size_t i, j;
275     const char *argv_empty[] = {NULL};
276 
277     /* Open and parse the script. */
278     if (access(file, R_OK) == 0)
279         path = bstrdup(file);
280     else {
281         path = test_file_path(file);
282         if (path == NULL)
283             bail("cannot find PAM script %s", file);
284     }
285     script = fopen(path, "r");
286     if (script == NULL)
287         sysbail("cannot open %s", path);
288     work = parse_script(script, config);
289     fclose(script);
290     diag("Starting %s", file);
291     if (work->prompts != NULL) {
292         conv.conv = converse;
293         conv.appdata_ptr = work->prompts;
294     }
295 
296     /* Initialize PAM. */
297     status = pam_start("test", config->user, &conv, &pamh);
298     if (status != PAM_SUCCESS)
299         sysbail("cannot create PAM handle");
300     if (config->authtok != NULL)
301         pamh->authtok = bstrdup(config->authtok);
302     if (config->oldauthtok != NULL)
303         pamh->oldauthtok = bstrdup(config->oldauthtok);
304 
305     /* Run the actions and check their return status. */
306     for (action = work->actions; action != NULL; action = action->next) {
307         if (work->options[action->group].argv == NULL)
308             status = (*action->call)(pamh, action->flags, 0, argv_empty);
309         else {
310             opts = &work->options[action->group];
311             status = (*action->call)(pamh, action->flags, opts->argc,
312                                      (const char **) opts->argv);
313         }
314         is_int(action->status, status, "status for %s", action->name);
315     }
316     output = pam_output();
317     check_output(work->output, output);
318     pam_output_free(output);
319 
320     /* If we have a test callback, call it now. */
321     if (config->callback != NULL)
322         config->callback(pamh, config, config->data);
323 
324     /* Free memory and return. */
325     pam_end(pamh, work->end_flags);
326     action = work->actions;
327     while (action != NULL) {
328         free(action->name);
329         oaction = action;
330         action = action->next;
331         free(oaction);
332     }
333     for (i = 0; i < ARRAY_SIZE(work->options); i++)
334         if (work->options[i].argv != NULL) {
335             for (j = 0; work->options[i].argv[j] != NULL; j++)
336                 free(work->options[i].argv[j]);
337             free(work->options[i].argv);
338         }
339     if (work->output)
340         pam_output_free(work->output);
341     if (work->prompts != NULL) {
342         for (i = 0; i < work->prompts->size; i++) {
343             free(work->prompts->prompts[i].prompt);
344             free(work->prompts->prompts[i].response);
345         }
346         free(work->prompts->prompts);
347         free(work->prompts);
348     }
349     free(work);
350     free(path);
351 }
352 
353 
354 /*
355  * Check a filename for acceptable characters.  Returns true if the file
356  * consists solely of [a-zA-Z0-9-] and false otherwise.
357  */
358 static bool
valid_filename(const char * filename)359 valid_filename(const char *filename)
360 {
361     const char *p;
362 
363     for (p = filename; *p != '\0'; p++) {
364         if (*p >= 'A' && *p <= 'Z')
365             continue;
366         if (*p >= 'a' && *p <= 'z')
367             continue;
368         if (*p >= '0' && *p <= '9')
369             continue;
370         if (*p == '-')
371             continue;
372         return false;
373     }
374     return true;
375 }
376 
377 
378 /*
379  * The same as run_script, but run every script found in the given directory,
380  * skipping file names that contain characters other than alphanumerics and -.
381  */
382 void
run_script_dir(const char * dir,const struct script_config * config)383 run_script_dir(const char *dir, const struct script_config *config)
384 {
385     DIR *handle;
386     struct dirent *entry;
387     const char *path;
388     char *file;
389 
390     if (access(dir, R_OK) == 0)
391         path = dir;
392     else
393         path = test_file_path(dir);
394     handle = opendir(path);
395     if (handle == NULL)
396         sysbail("cannot open directory %s", dir);
397     errno = 0;
398     while ((entry = readdir(handle)) != NULL) {
399         if (!valid_filename(entry->d_name))
400             continue;
401         basprintf(&file, "%s/%s", path, entry->d_name);
402         run_script(file, config);
403         free(file);
404         errno = 0;
405     }
406     if (errno != 0)
407         sysbail("cannot read directory %s", dir);
408     closedir(handle);
409     if (path != dir)
410         test_file_path_free((char *) path);
411 }
412