]> gitweb.michael.orlitzky.com - apply-default-acl.git/blob - src/apply-default-acl.c
Add a new command-line option, --no-exec-mask, that disables the exec bit masking...
[apply-default-acl.git] / src / apply-default-acl.c
1 /* On Linux, ftw.h needs this special voodoo to work. */
2 #define _XOPEN_SOURCE 500
3
4 #include <errno.h>
5 #include <ftw.h> /* nftw() et al. */
6 #include <getopt.h>
7 #include <libgen.h> /* dirname() */
8 #include <limits.h> /* PATH_MAX */
9 #include <stdbool.h>
10 #include <stdio.h>
11 #include <stdlib.h>
12 #include <string.h>
13 #include <sys/stat.h>
14 #include <unistd.h>
15
16 /* ACLs */
17 #include <acl/libacl.h> /* acl_get_perm, not portable */
18 #include <sys/types.h>
19 #include <sys/acl.h>
20
21
22 /* Command-line options */
23 static bool no_exec_mask = false;
24
25
26 mode_t get_mode(const char* path) {
27 /*
28 * Get the mode bits from path.
29 */
30 if (path == NULL) {
31 errno = ENOENT;
32 return -1;
33 }
34
35 struct stat s;
36 int result = stat(path, &s);
37
38 if (result == 0) {
39 return s.st_mode;
40 }
41 else {
42 /* errno will be set already by stat() */
43 return result;
44 }
45 }
46
47
48 bool is_regular_file(const char* path) {
49 /*
50 * Returns true if path is a regular file, false otherwise.
51 */
52 if (path == NULL) {
53 return false;
54 }
55
56 struct stat s;
57 int result = stat(path, &s);
58 if (result == 0) {
59 return S_ISREG(s.st_mode);
60 }
61 else {
62 return false;
63 }
64 }
65
66 bool is_directory(const char* path) {
67 /*
68 * Returns true if path is a directory, false otherwise.
69 */
70 if (path == NULL) {
71 return false;
72 }
73
74 struct stat s;
75 int result = stat(path, &s);
76 if (result == 0) {
77 return S_ISDIR(s.st_mode);
78 }
79 else {
80 return false;
81 }
82 }
83
84
85
86 int acl_set_entry(acl_t* aclp,
87 acl_entry_t entry) {
88 /*
89 * Update or create the given entry.
90 */
91
92 acl_tag_t entry_tag;
93 int gt_result = acl_get_tag_type(entry, &entry_tag);
94 if (gt_result == -1) {
95 perror("acl_set_entry (acl_get_tag_type)");
96 return -1;
97 }
98
99 acl_permset_t entry_permset;
100 int ps_result = acl_get_permset(entry, &entry_permset);
101 if (ps_result == -1) {
102 perror("acl_set_entry (acl_get_permset)");
103 return -1;
104 }
105
106 acl_entry_t existing_entry;
107 /* Loop through the given ACL looking for matching entries. */
108 int result = acl_get_entry(*aclp, ACL_FIRST_ENTRY, &existing_entry);
109
110 while (result == 1) {
111 acl_tag_t existing_tag = ACL_UNDEFINED_TAG;
112 int tag_result = acl_get_tag_type(existing_entry, &existing_tag);
113
114 if (tag_result == -1) {
115 perror("set_acl_tag_permset (acl_get_tag_type)");
116 return -1;
117 }
118
119 if (existing_tag == entry_tag) {
120 if (entry_tag == ACL_USER_OBJ ||
121 entry_tag == ACL_GROUP_OBJ ||
122 entry_tag == ACL_OTHER) {
123 /* Only update for these three since all other tags will have
124 been wiped. */
125 acl_permset_t existing_permset;
126 int gep_result = acl_get_permset(existing_entry, &existing_permset);
127 if (gep_result == -1) {
128 perror("acl_set_entry (acl_get_permset)");
129 return -1;
130 }
131
132 int s_result = acl_set_permset(existing_entry, entry_permset);
133 if (s_result == -1) {
134 perror("acl_set_entry (acl_set_permset)");
135 return -1;
136 }
137
138 return 1;
139 }
140
141 }
142
143 result = acl_get_entry(*aclp, ACL_NEXT_ENTRY, &existing_entry);
144 }
145
146 /* This catches both the initial acl_get_entry and the ones at the
147 end of the loop. */
148 if (result == -1) {
149 perror("acl_set_entry (acl_get_entry)");
150 return -1;
151 }
152
153 /* If we've made it this far, we need to add a new entry to the
154 ACL. */
155 acl_entry_t new_entry;
156
157 /* We allocate memory here that we should release! */
158 int c_result = acl_create_entry(aclp, &new_entry);
159 if (c_result == -1) {
160 perror("acl_set_entry (acl_create_entry)");
161 return -1;
162 }
163
164 int st_result = acl_set_tag_type(new_entry, entry_tag);
165 if (st_result == -1) {
166 perror("acl_set_entry (acl_set_tag_type)");
167 return -1;
168 }
169
170 int s_result = acl_set_permset(new_entry, entry_permset);
171 if (s_result == -1) {
172 perror("acl_set_entry (acl_set_permset)");
173 return -1;
174 }
175
176 if (entry_tag == ACL_USER || entry_tag == ACL_GROUP) {
177 /* We need to set the qualifier too. */
178 void* entry_qual = acl_get_qualifier(entry);
179 if (entry_qual == (void*)NULL) {
180 perror("acl_set_entry (acl_get_qualifier)");
181 return -1;
182 }
183
184 int sq_result = acl_set_qualifier(new_entry, entry_qual);
185 if (sq_result == -1) {
186 perror("acl_set_entry (acl_set_qualifier)");
187 return -1;
188 }
189 }
190
191 return 1;
192 }
193
194
195
196 int acl_entry_count(acl_t* acl) {
197 /*
198 * Return the number of entries in acl, or -1 on error.
199 */
200 acl_entry_t entry;
201 int entry_count = 0;
202 int result = acl_get_entry(*acl, ACL_FIRST_ENTRY, &entry);
203
204 while (result == 1) {
205 entry_count++;
206 result = acl_get_entry(*acl, ACL_NEXT_ENTRY, &entry);
207 }
208
209 if (result == -1) {
210 perror("acl_is_minimal (acl_get_entry)");
211 return -1;
212 }
213
214 return entry_count;
215 }
216
217
218 int acl_is_minimal(acl_t* acl) {
219 /* An ACL is minimal if it has fewer than four entries. Return 0 for
220 * false, 1 for true, and -1 on error.
221 */
222
223 int ec = acl_entry_count(acl);
224 if (ec == -1) {
225 perror("acl_is_minimal (acl_entry_count)");
226 return -1;
227 }
228
229 if (ec < 4) {
230 return 1;
231 }
232 else {
233 return 0;
234 }
235 }
236
237
238 int acl_execute_masked(const char* path) {
239 /* Returns 1 i the given path has an ACL mask which denies
240 execute. Returns 0 if it does not (or if it has no ACL/mask at
241 all), and -1 on error. */
242
243 acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS);
244
245 if (acl == (acl_t)NULL) {
246 return 0;
247 }
248
249 /* Our return value. */
250 int result = 0;
251
252 acl_entry_t entry;
253 int ge_result = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
254
255 while (ge_result == 1) {
256 acl_tag_t tag = ACL_UNDEFINED_TAG;
257 int tag_result = acl_get_tag_type(entry, &tag);
258
259 if (tag_result == -1) {
260 perror("acl_execute_masked (acl_get_tag_type)");
261 result = -1;
262 goto cleanup;
263 }
264
265 if (tag == ACL_MASK) {
266 /* This is the mask entry, get its permissions, and see if
267 execute is specified. */
268 acl_permset_t permset;
269
270 int ps_result = acl_get_permset(entry, &permset);
271 if (ps_result == -1) {
272 perror("acl_execute_masked (acl_get_permset)");
273 result = -1;
274 goto cleanup;
275 }
276
277 int gp_result = acl_get_perm(permset, ACL_EXECUTE);
278 if (gp_result == -1) {
279 perror("acl_execute_masked (acl_get_perm)");
280 result = -1;
281 goto cleanup;
282 }
283
284 if (gp_result == 0) {
285 /* No execute bit set in the mask; execute not allowed. */
286 return 1;
287 }
288 }
289
290 ge_result = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry);
291 }
292
293 cleanup:
294 acl_free(acl);
295 return result;
296 }
297
298
299 int any_can_execute_or_dir(const char* path) {
300 /* If the given path is a directory, returns 1. Otherwise, returns 1
301 * if any ACL entry has EFFECTIVE execute access, 0 if none do, and
302 * -1 on error.
303 *
304 * This is meant to somewhat mimic setfacl's handling of the capital
305 * 'X' perm, which allows execute access if the target is a
306 * directory or someone can already execute it. We differ in that we
307 * check the effective execute rather than just the execute bits.
308 */
309
310 if (is_directory(path)) {
311 /* That was easy... */
312 return 1;
313 }
314
315 acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS);
316
317 if (acl == (acl_t)NULL) {
318 return 0;
319 }
320
321 /* Our return value. */
322 int result = 0;
323
324 if (acl_is_minimal(&acl)) {
325 mode_t mode = get_mode(path);
326 if (mode & (S_IXUSR | S_IXOTH | S_IXGRP)) {
327 result = 1;
328 goto cleanup;
329 }
330 else {
331 result = 0;
332 goto cleanup;
333 }
334 }
335
336 acl_entry_t entry;
337 int ge_result = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
338
339 while (ge_result == 1) {
340 acl_permset_t permset;
341
342 int ps_result = acl_get_permset(entry, &permset);
343 if (ps_result == -1) {
344 perror("any_can_execute_or_dir (acl_get_permset)");
345 result = -1;
346 goto cleanup;
347 }
348
349 int gp_result = acl_get_perm(permset, ACL_EXECUTE);
350 if (gp_result == -1) {
351 perror("any_can_execute_or_dir (acl_get_perm)");
352 result = -1;
353 goto cleanup;
354 }
355
356 if (gp_result == 1) {
357 /* Only return one if this execute bit is not masked. */
358 if (acl_execute_masked(path) != 1) {
359 result = 1;
360 goto cleanup;
361 }
362 }
363
364 ge_result = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry);
365 }
366
367 if (ge_result == -1) {
368 perror("any_can_execute_or_dir (acl_get_entry)");
369 result = -1;
370 goto cleanup;
371 }
372
373 cleanup:
374 acl_free(acl);
375 return result;
376 }
377
378
379 int inherit_default_acl(const char* path, const char* parent) {
380 /* Inherit the default ACL from parent to path. This overwrites any
381 * existing default ACL. Returns 1 for success, 0 for failure, and
382 * -1 on error.
383 */
384
385 /* Our return value. */
386 int result = 1;
387
388 if (path == NULL) {
389 errno = ENOENT;
390 return -1;
391 }
392
393 if (!is_directory(path) || !is_directory(parent)) {
394 return 0;
395 }
396
397 acl_t parent_acl = acl_get_file(parent, ACL_TYPE_DEFAULT);
398 if (parent_acl == (acl_t)NULL) {
399 return 0;
400 }
401
402 acl_t path_acl = acl_dup(parent_acl);
403
404 if (path_acl == (acl_t)NULL) {
405 perror("inherit_default_acl (acl_dup)");
406 acl_free(parent_acl);
407 return -1;
408 }
409
410 int sf_result = acl_set_file(path, ACL_TYPE_DEFAULT, path_acl);
411 if (sf_result == -1) {
412 perror("inherit_default_acl (acl_set_file)");
413 result = -1;
414 goto cleanup;
415 }
416
417 cleanup:
418 acl_free(path_acl);
419 return result;
420 }
421
422
423 int wipe_acls(const char* path) {
424 /* Remove ACL_USER, ACL_GROUP, and ACL_MASK entries from
425 path. Returns 1 for success, 0 for failure, and -1 on error. */
426
427 if (path == NULL) {
428 errno = ENOENT;
429 return -1;
430 }
431
432 /* Finally, remove individual named/mask entries. */
433 acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS);
434 if (acl == (acl_t)NULL) {
435 perror("wipe_acls (acl_get_file)");
436 return -1;
437 }
438
439 /* Our return value. */
440 int result = 1;
441
442 acl_entry_t entry;
443 int ge_result = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
444
445 while (ge_result == 1) {
446 int d_result = acl_delete_entry(acl, entry);
447 if (d_result == -1) {
448 perror("wipe_acls (acl_delete_entry)");
449 result = -1;
450 goto cleanup;
451 }
452
453 ge_result = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry);
454 }
455
456 /* Catches the first acl_get_entry as well as the ones at the end of
457 the loop. */
458 if (ge_result == -1) {
459 perror("wipe_acls (acl_get_entry)");
460 result = -1;
461 goto cleanup;
462 }
463
464 int sf_result = acl_set_file(path, ACL_TYPE_ACCESS, acl);
465 if (sf_result == -1) {
466 perror("wipe_acls (acl_set_file)");
467 result = -1;
468 goto cleanup;
469 }
470
471 cleanup:
472 acl_free(acl);
473 return result;
474 }
475
476
477 int apply_default_acl(const char* path) {
478 /* Really apply the default ACL by looping through it. Returns one
479 * for success, zero for failure (i.e. no ACL), and -1 on unexpected
480 * errors.
481
482 */
483 if (path == NULL) {
484 return 0;
485 }
486
487 if (!is_regular_file(path) && !is_directory(path)) {
488 return 0;
489 }
490
491 /* dirname mangles its argument */
492 char path_copy[PATH_MAX];
493 strncpy(path_copy, path, PATH_MAX-1);
494 path_copy[PATH_MAX-1] = 0;
495
496 char* parent = dirname(path_copy);
497 if (!is_directory(parent)) {
498 /* Make sure dirname() did what we think it did. */
499 return 0;
500 }
501
502 /* Default to not masking the exec bit; i.e. applying the default
503 ACL literally. If --no-exec-mask was not specified, then we try
504 to "guess" whether or not to mask the exec bit. */
505 bool allow_exec = true;
506
507 if (!no_exec_mask) {
508 int ace_result = any_can_execute_or_dir(path);
509
510 if (ace_result == -1) {
511 perror("apply_default_acl (any_can_execute_or_dir)");
512 return -1;
513 }
514 allow_exec = (bool)ace_result;
515 }
516
517 acl_t defacl = acl_get_file(parent, ACL_TYPE_DEFAULT);
518
519 if (defacl == (acl_t)NULL) {
520 perror("apply_default_acl (acl_get_file)");
521 return -1;
522 }
523
524 /* Our return value. */
525 int result = 1;
526
527 int wipe_result = wipe_acls(path);
528 if (wipe_result == -1) {
529 perror("apply_default_acl (wipe_acls)");
530 result = -1;
531 goto cleanup;
532 }
533
534 /* Do this after wipe_acls(), otherwise we'll overwrite the wiped
535 ACL with this one. */
536 acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS);
537 if (acl == (acl_t)NULL) {
538 perror("apply_default_acl (acl_get_file)");
539 return -1;
540 }
541
542 /* If it's a directory, inherit the parent's default. */
543 int inherit_result = inherit_default_acl(path, parent);
544 if (inherit_result == -1) {
545 perror("apply_default_acl (inherit_acls)");
546 result = -1;
547 goto cleanup;
548 }
549
550 acl_entry_t entry;
551 int ge_result = acl_get_entry(defacl, ACL_FIRST_ENTRY, &entry);
552
553 while (ge_result == 1) {
554 acl_tag_t tag = ACL_UNDEFINED_TAG;
555 int tag_result = acl_get_tag_type(entry, &tag);
556
557 if (tag_result == -1) {
558 perror("apply_default_acl (acl_get_tag_type)");
559 result = -1;
560 goto cleanup;
561 }
562
563
564 /* We've got an entry/tag from the default ACL. Get its permset. */
565 acl_permset_t permset;
566 int ps_result = acl_get_permset(entry, &permset);
567 if (ps_result == -1) {
568 perror("apply_default_acl (acl_get_permset)");
569 result = -1;
570 goto cleanup;
571 }
572
573 /* If this is a default mask, fix it up. */
574 if (tag == ACL_MASK ||
575 tag == ACL_USER_OBJ ||
576 tag == ACL_GROUP_OBJ ||
577 tag == ACL_OTHER) {
578
579 if (!allow_exec) {
580 /* The mask doesn't affect acl_user_obj, acl_group_obj (in
581 minimal ACLs) or acl_other entries, so if execute should be
582 masked, we have to do it manually. */
583 int d_result = acl_delete_perm(permset, ACL_EXECUTE);
584 if (d_result == -1) {
585 perror("apply_default_acl (acl_delete_perm)");
586 result = -1;
587 goto cleanup;
588 }
589
590 int sp_result = acl_set_permset(entry, permset);
591 if (sp_result == -1) {
592 perror("apply_default_acl (acl_set_permset)");
593 result = -1;
594 goto cleanup;
595 }
596 }
597 }
598
599 /* Finally, add the permset to the access ACL. */
600 int set_result = acl_set_entry(&acl, entry);
601 if (set_result == -1) {
602 perror("apply_default_acl (acl_set_entry)");
603 result = -1;
604 goto cleanup;
605 }
606
607 ge_result = acl_get_entry(defacl, ACL_NEXT_ENTRY, &entry);
608 }
609
610 /* Catches the first acl_get_entry as well as the ones at the end of
611 the loop. */
612 if (ge_result == -1) {
613 perror("apply_default_acl (acl_get_entry)");
614 result = -1;
615 goto cleanup;
616 }
617
618 int sf_result = acl_set_file(path, ACL_TYPE_ACCESS, acl);
619 if (sf_result == -1) {
620 perror("apply_default_acl (acl_set_file)");
621 result = -1;
622 goto cleanup;
623 }
624
625 cleanup:
626 acl_free(defacl);
627 return result;
628 }
629
630
631 void usage(char* program_name) {
632 /*
633 * Print usage information.
634 */
635 printf("Apply any applicable default ACLs to the given files or "
636 "directories.\n\n");
637 printf("Usage: %s [flags] <target1> [<target2> [ <target3>...]]\n\n",
638 program_name);
639 printf("Flags:\n");
640 printf(" -h, --help Print this help message\n");
641 printf(" -r, --recursive Act on any given directories recursively\n");
642 printf(" -x, --no-exec-mask Apply execute permissions unconditionally\n");
643
644 return;
645 }
646
647
648 int apply_default_acl_nftw(const char *target,
649 const struct stat *s,
650 int info,
651 struct FTW *ftw) {
652 /* A wrapper around the apply_default_acl() function for use with
653 * nftw(). We need to adjust the return value so that nftw() doesn't
654 * think we've failed.
655 */
656 bool reapp_result = apply_default_acl(target);
657 if (reapp_result) {
658 return 0;
659 }
660 else {
661 return 1;
662 }
663 }
664
665
666 bool apply_default_acl_recursive(const char *target) {
667 /* Attempt to apply default ACLs recursively. If target is a
668 * directory, we recurse through its entries. If not, we just
669 * apply the default ACL to target.
670 *
671 * We ignore symlinks for consistency with chmod -r.
672 *
673 */
674 if (!is_directory(target)) {
675 return apply_default_acl(target);
676 }
677
678 int max_levels = 256;
679 int flags = FTW_PHYS; /* Don't follow links. */
680
681 int nftw_result = nftw(target,
682 apply_default_acl_nftw,
683 max_levels,
684 flags);
685
686 if (nftw_result == 0) {
687 /* Success */
688 return true;
689 }
690
691 /* nftw will return -1 on error, or if the supplied function
692 * (apply_default_acl_nftw) returns a non-zero result, nftw will
693 * return that.
694 */
695 if (nftw_result == -1) {
696 perror("apply_default_acl_recursive (nftw)");
697 }
698
699 return false;
700 }
701
702
703 int main(int argc, char* argv[]) {
704 /*
705 * Call apply_default_acl on each command-line argument.
706 */
707 if (argc < 2) {
708 usage(argv[0]);
709 return EXIT_FAILURE;
710 }
711
712 bool recursive = false;
713 /* bool no_exec_mask is declared static/global */
714
715 struct option long_options[] = {
716 /* These options set a flag. */
717 {"help", no_argument, NULL, 'h'},
718 {"recursive", no_argument, NULL, 'r'},
719 {"no-exec-mask", no_argument, NULL, 'x'},
720 {NULL, 0, NULL, 0}
721 };
722
723 int opt = 0;
724
725 while ((opt = getopt_long(argc, argv, "hrx", long_options, NULL)) != -1) {
726 switch (opt) {
727 case 'h':
728 usage(argv[0]);
729 return EXIT_SUCCESS;
730 case 'r':
731 recursive = true;
732 break;
733 case 'x':
734 no_exec_mask = true;
735 break;
736 default:
737 usage(argv[0]);
738 return EXIT_FAILURE;
739 }
740 }
741
742 int result = EXIT_SUCCESS;
743
744 int arg_index = 1;
745 for (arg_index = optind; arg_index < argc; arg_index++) {
746 const char* target = argv[arg_index];
747 bool reapp_result = false;
748
749 if (recursive) {
750 reapp_result = apply_default_acl_recursive(target);
751 }
752 else {
753 /* It's either normal file, or we're not operating recursively. */
754 reapp_result = apply_default_acl(target);
755 }
756
757 if (!reapp_result) {
758 result = EXIT_FAILURE;
759 }
760 }
761
762 return result;
763 }