Directory: | cvmfs/ |
---|---|
File: | cvmfs/acl.cc |
Date: | 2025-07-13 02:35:07 |
Exec | Total | Coverage | |
---|---|---|---|
Lines: | 138 | 151 | 91.4% |
Branches: | 104 | 132 | 78.8% |
Line | Branch | Exec | Source |
---|---|---|---|
1 | /** | ||
2 | * This file is part of the CernVM File System. | ||
3 | */ | ||
4 | |||
5 | #include "acl.h" | ||
6 | |||
7 | #include <string.h> | ||
8 | |||
9 | #include <algorithm> | ||
10 | #include <cassert> | ||
11 | #include <cstring> | ||
12 | #include <vector> | ||
13 | |||
14 | #include "util/posix.h" | ||
15 | |||
16 | using namespace std; // NOLINT | ||
17 | |||
18 | #ifdef COMPARE_TO_LIBACL | ||
19 | #include "acl/libacl.h" | ||
20 | #else // COMPARE_TO_LIBACL | ||
21 | |||
22 | // ACL permission bits | ||
23 | #define ACL_READ (0x04) | ||
24 | #define ACL_WRITE (0x02) | ||
25 | #define ACL_EXECUTE (0x01) | ||
26 | |||
27 | // ACL tag types | ||
28 | #define ACL_UNDEFINED_TAG (0x00) | ||
29 | #define ACL_USER_OBJ (0x01) | ||
30 | #define ACL_USER (0x02) | ||
31 | #define ACL_GROUP_OBJ (0x04) | ||
32 | #define ACL_GROUP (0x08) | ||
33 | #define ACL_MASK (0x10) | ||
34 | #define ACL_OTHER (0x20) | ||
35 | |||
36 | // ACL qualifier constants | ||
37 | #define ACL_UNDEFINED_ID ((id_t) - 1) | ||
38 | |||
39 | #endif // COMPARE_TO_LIBACL | ||
40 | |||
41 | #define ACL_EA_VERSION 0x0002 | ||
42 | |||
43 | // ACL data structures | ||
44 | struct acl_ea_entry { | ||
45 | u_int16_t e_tag; | ||
46 | u_int16_t e_perm; | ||
47 | u_int32_t e_id; | ||
48 | |||
49 | // implements sorting compatible with libacl | ||
50 | 1209 | bool operator<(const acl_ea_entry &other) const { | |
51 |
2/2✓ Branch 0 taken 1131 times.
✓ Branch 1 taken 78 times.
|
1209 | if (e_tag != other.e_tag) { |
52 | 1131 | return e_tag < other.e_tag; | |
53 | } | ||
54 | 78 | return e_id < other.e_id; | |
55 | } | ||
56 | }; | ||
57 | |||
58 | struct acl_ea_header { | ||
59 | u_int32_t a_version; | ||
60 | acl_ea_entry a_entries[0]; | ||
61 | }; | ||
62 | |||
63 | 286 | static int acl_from_text_to_string_entries(const string &acl_string, | |
64 | vector<string> &string_entries) { | ||
65 | 286 | std::size_t entry_pos = 0; | |
66 |
2/2✓ Branch 0 taken 1131 times.
✓ Branch 1 taken 182 times.
|
1313 | while (entry_pos != string::npos) { |
67 | size_t entry_length; | ||
68 | size_t next_pos; | ||
69 | 1131 | size_t const sep_pos = acl_string.find_first_of(",\n", entry_pos); | |
70 |
2/2✓ Branch 0 taken 286 times.
✓ Branch 1 taken 845 times.
|
1131 | if (sep_pos == string::npos) { |
71 |
2/2✓ Branch 1 taken 182 times.
✓ Branch 2 taken 104 times.
|
286 | if (acl_string.length() > entry_pos) { |
72 | 182 | entry_length = acl_string.length() - entry_pos; | |
73 | 182 | next_pos = string::npos; | |
74 | } else { | ||
75 | // we've just looked past a trailing delimiter | ||
76 | 104 | break; | |
77 | } | ||
78 | } else { | ||
79 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 845 times.
|
845 | assert(sep_pos >= entry_pos); |
80 | 845 | entry_length = sep_pos - entry_pos; | |
81 | 845 | next_pos = sep_pos + 1; | |
82 | } | ||
83 |
2/2✓ Branch 0 taken 143 times.
✓ Branch 1 taken 884 times.
|
1027 | if (entry_length == 0) { |
84 | // libacl tolerates excessive whitespace but not excessive delimiters. | ||
85 | // It's simpler for us to treat whitespace as delimiters. | ||
86 | 143 | entry_pos = next_pos; | |
87 | 143 | continue; | |
88 | } | ||
89 |
1/2✓ Branch 1 taken 884 times.
✗ Branch 2 not taken.
|
884 | string entry(acl_string, entry_pos, entry_length); |
90 | 884 | entry_pos = next_pos; | |
91 | |||
92 | // search for '#'-starting comment, discard if found | ||
93 | 884 | size_t const comment_pos = entry.find('#'); | |
94 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 884 times.
|
884 | if (comment_pos != string::npos) { |
95 | ✗ | entry = string(entry, 0, comment_pos); | |
96 | } | ||
97 | |||
98 | // TODO(autkin): trim whitespace on both ends | ||
99 | |||
100 | // discard empty lines | ||
101 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 884 times.
|
884 | if (entry.length() == 0) { |
102 | ✗ | continue; | |
103 | } | ||
104 | |||
105 |
1/2✓ Branch 1 taken 884 times.
✗ Branch 2 not taken.
|
884 | string_entries.push_back(entry); |
106 |
1/2✓ Branch 1 taken 884 times.
✗ Branch 2 not taken.
|
884 | } |
107 | 286 | return 0; | |
108 | } | ||
109 | |||
110 | 806 | static int acl_parms_from_text(const string &str, u_int16_t *perms) { | |
111 | // Currently unsupported syntax features found in setfacl: | ||
112 | // - X (capital x) | ||
113 | // - numeric syntax | ||
114 | // See "man 1 setfacl", "The perms field is..." | ||
115 | |||
116 | 806 | *perms = 0; | |
117 |
2/2✓ Branch 5 taken 1456 times.
✓ Branch 6 taken 806 times.
|
2262 | for (const char &c : str) { |
118 |
4/5✓ Branch 0 taken 533 times.
✓ Branch 1 taken 325 times.
✓ Branch 2 taken 156 times.
✓ Branch 3 taken 442 times.
✗ Branch 4 not taken.
|
1456 | switch (c) { |
119 | 533 | case 'r': | |
120 | 533 | *perms |= ACL_READ; | |
121 | 533 | break; | |
122 | 325 | case 'w': | |
123 | 325 | *perms |= ACL_WRITE; | |
124 | 325 | break; | |
125 | 156 | case 'x': | |
126 | 156 | *perms |= ACL_EXECUTE; | |
127 | 156 | break; | |
128 | 442 | case '-': | |
129 | 442 | break; | |
130 | ✗ | default: | |
131 | ✗ | return EINVAL; | |
132 | } | ||
133 | } | ||
134 | 806 | return 0; | |
135 | } | ||
136 | |||
137 | 819 | static int acl_entry_from_text(const string &str, acl_ea_entry &entry) { | |
138 | // break down to 3 fields by ':' | ||
139 | // type:qualifier:permissions according to terminology | ||
140 | // e_tag:e_id:e_perm are acl_ea_entry field names | ||
141 | 819 | size_t sep_pos = str.find(':'); | |
142 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 819 times.
|
819 | if (sep_pos == string::npos) { |
143 | ✗ | return EINVAL; | |
144 | } | ||
145 |
1/2✓ Branch 1 taken 819 times.
✗ Branch 2 not taken.
|
819 | string const type(str, 0, sep_pos); |
146 | 819 | size_t next_field_pos = sep_pos + 1; | |
147 | 819 | sep_pos = str.find(':', next_field_pos); | |
148 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 819 times.
|
819 | if (sep_pos == string::npos) { |
149 | ✗ | return EINVAL; | |
150 | } | ||
151 |
1/2✓ Branch 1 taken 819 times.
✗ Branch 2 not taken.
|
819 | string const qualifier(str, next_field_pos, sep_pos - next_field_pos); |
152 | 819 | next_field_pos = sep_pos + 1; | |
153 |
1/2✓ Branch 2 taken 819 times.
✗ Branch 3 not taken.
|
819 | string const permissions(str, next_field_pos); |
154 | |||
155 |
6/6✓ Branch 1 taken 806 times.
✓ Branch 2 taken 13 times.
✓ Branch 4 taken 312 times.
✓ Branch 5 taken 494 times.
✓ Branch 6 taken 325 times.
✓ Branch 7 taken 494 times.
|
819 | if (!type.compare("user") || !type.compare("u")) { |
156 |
2/2✓ Branch 1 taken 195 times.
✓ Branch 2 taken 130 times.
|
325 | entry.e_tag = qualifier.empty() ? ACL_USER_OBJ : ACL_USER; |
157 |
6/6✓ Branch 1 taken 455 times.
✓ Branch 2 taken 39 times.
✓ Branch 4 taken 195 times.
✓ Branch 5 taken 260 times.
✓ Branch 6 taken 234 times.
✓ Branch 7 taken 260 times.
|
494 | } else if (!type.compare("group") || !type.compare("g")) { |
158 |
2/2✓ Branch 1 taken 182 times.
✓ Branch 2 taken 52 times.
|
234 | entry.e_tag = qualifier.empty() ? ACL_GROUP_OBJ : ACL_GROUP; |
159 |
6/6✓ Branch 1 taken 247 times.
✓ Branch 2 taken 13 times.
✓ Branch 4 taken 169 times.
✓ Branch 5 taken 78 times.
✓ Branch 6 taken 182 times.
✓ Branch 7 taken 78 times.
|
260 | } else if (!type.compare("other") || !type.compare("o")) { |
160 | 182 | entry.e_tag = ACL_OTHER; | |
161 |
6/6✓ Branch 1 taken 39 times.
✓ Branch 2 taken 39 times.
✓ Branch 4 taken 26 times.
✓ Branch 5 taken 13 times.
✓ Branch 6 taken 65 times.
✓ Branch 7 taken 13 times.
|
78 | } else if (!type.compare("mask") || !type.compare("m")) { |
162 | 65 | entry.e_tag = ACL_MASK; | |
163 | } else { | ||
164 | 13 | return EINVAL; | |
165 | } | ||
166 | 806 | entry.e_tag = htole16(entry.e_tag); | |
167 | |||
168 |
2/2✓ Branch 1 taken 624 times.
✓ Branch 2 taken 182 times.
|
806 | if (qualifier.empty()) { |
169 | 624 | entry.e_id = ACL_UNDEFINED_ID; | |
170 | } else { | ||
171 | char *at_null_terminator_if_number; | ||
172 | 182 | long number = strtol(qualifier.c_str(), &at_null_terminator_if_number, 10); | |
173 |
2/2✓ Branch 0 taken 91 times.
✓ Branch 1 taken 91 times.
|
182 | if (*at_null_terminator_if_number != '\0') { |
174 | bool ok; | ||
175 |
2/2✓ Branch 1 taken 65 times.
✓ Branch 2 taken 26 times.
|
91 | if (entry.e_tag == htole16(ACL_USER)) { |
176 | [[maybe_unused]] gid_t main_gid; | ||
177 | uid_t uid; | ||
178 |
1/2✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
|
65 | ok = GetUidOf(qualifier, &uid, &main_gid); |
179 | 65 | number = uid; | |
180 |
1/2✓ Branch 1 taken 26 times.
✗ Branch 2 not taken.
|
26 | } else if (entry.e_tag == htole16(ACL_GROUP)) { |
181 | gid_t gid; | ||
182 |
1/2✓ Branch 1 taken 26 times.
✗ Branch 2 not taken.
|
26 | ok = GetGidOf(qualifier, &gid); |
183 | 26 | number = gid; | |
184 | } else { | ||
185 | ✗ | assert(false); | |
186 | } | ||
187 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 91 times.
|
91 | if (!ok) { |
188 | ✗ | return EINVAL; | |
189 | } | ||
190 | } | ||
191 | 182 | entry.e_id = htole32(number); | |
192 | } | ||
193 | |||
194 | // parse perms | ||
195 | u_int16_t host_byteorder_perms; | ||
196 | int ret; | ||
197 | 806 | ret = acl_parms_from_text(permissions, &host_byteorder_perms); | |
198 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 806 times.
|
806 | if (ret) { |
199 | ✗ | return ret; | |
200 | } | ||
201 | 806 | entry.e_perm = htole16(host_byteorder_perms); | |
202 | |||
203 | 806 | return 0; | |
204 | 819 | } | |
205 | |||
206 | 273 | static bool acl_valid_builtin(const vector<acl_ea_entry> &entries) { | |
207 | // From man acl_valid: | ||
208 | // The three required entries ACL_USER_OBJ, ACL_GROUP_OBJ, and ACL_OTHER must | ||
209 | // exist exactly once in the ACL. | ||
210 | // | ||
211 | // If the ACL contains any ACL_USER or ACL_GROUP entries, then an ACL_MASK | ||
212 | // entry is also required. | ||
213 | // | ||
214 | // The ACL may contain at most one ACL_MASK entry. | ||
215 | // | ||
216 | // The user identifiers must be unique among all entries of type ACL_USER. | ||
217 | // The group identifiers must be unique among all entries of type ACL_GROUP. | ||
218 | |||
219 | 273 | bool types_met[ACL_OTHER + 1] = { | |
220 | false, | ||
221 | }; | ||
222 | |||
223 |
2/2✓ Branch 4 taken 754 times.
✓ Branch 5 taken 260 times.
|
1014 | for (auto entry_it = entries.begin(); entry_it != entries.end(); ++entry_it) { |
224 | 754 | const acl_ea_entry &e = *entry_it; | |
225 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 754 times.
|
754 | assert(e.e_tag <= ACL_OTHER); |
226 | 754 | bool &type_met = types_met[e.e_tag]; | |
227 |
2/3✓ Branch 0 taken 598 times.
✓ Branch 1 taken 156 times.
✗ Branch 2 not taken.
|
754 | switch (e.e_tag) { |
228 | // at most one of these types | ||
229 | 598 | case ACL_USER_OBJ: | |
230 | case ACL_GROUP_OBJ: | ||
231 | case ACL_OTHER: | ||
232 | case ACL_MASK: | ||
233 |
2/2✓ Branch 0 taken 13 times.
✓ Branch 1 taken 585 times.
|
598 | if (type_met) { |
234 | 13 | return false; | |
235 | } else { | ||
236 | 585 | type_met = true; | |
237 | } | ||
238 | 585 | break; | |
239 | 156 | case ACL_USER: | |
240 | case ACL_GROUP: | ||
241 | 156 | type_met = true; | |
242 | 156 | break; | |
243 | ✗ | default: | |
244 | ✗ | assert(false); | |
245 | } | ||
246 | } | ||
247 |
4/4✓ Branch 0 taken 169 times.
✓ Branch 1 taken 91 times.
✓ Branch 2 taken 156 times.
✓ Branch 3 taken 13 times.
|
260 | if (!(types_met[ACL_USER_OBJ] && types_met[ACL_GROUP_OBJ] |
248 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 156 times.
|
156 | && types_met[ACL_OTHER])) { |
249 | 104 | return false; | |
250 | } | ||
251 |
6/6✓ Branch 0 taken 91 times.
✓ Branch 1 taken 65 times.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 78 times.
✓ Branch 4 taken 13 times.
✓ Branch 5 taken 65 times.
|
156 | if ((types_met[ACL_USER] || types_met[ACL_GROUP]) && !types_met[ACL_MASK]) { |
252 | 13 | return false; | |
253 | } | ||
254 | // TODO(autkin): ACL_USER, ACL_GROUP uniqueness checks. Not a pressing issue. | ||
255 | 143 | return true; | |
256 | } | ||
257 | |||
258 | 286 | int acl_from_text_to_xattr_value(const string &textual_acl, char *&o_binary_acl, | |
259 | size_t &o_size, bool &o_equiv_mode) { | ||
260 | int ret; | ||
261 | |||
262 | 286 | o_equiv_mode = true; | |
263 | |||
264 | // get individual textual entries from one big text | ||
265 | 286 | vector<string> string_entries; | |
266 |
1/2✓ Branch 1 taken 286 times.
✗ Branch 2 not taken.
|
286 | ret = acl_from_text_to_string_entries(textual_acl, string_entries); |
267 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
|
286 | if (ret) { |
268 | ✗ | return ret; | |
269 | } | ||
270 | |||
271 | // get individual entries in structural form | ||
272 | 286 | vector<acl_ea_entry> entries; | |
273 | 286 | for (auto string_it = string_entries.begin(); | |
274 |
2/2✓ Branch 2 taken 819 times.
✓ Branch 3 taken 273 times.
|
1092 | string_it != string_entries.end(); |
275 | 806 | ++string_it) { | |
276 | acl_ea_entry entry; | ||
277 |
1/2✓ Branch 2 taken 819 times.
✗ Branch 3 not taken.
|
819 | ret = acl_entry_from_text(*string_it, entry); |
278 |
2/2✓ Branch 0 taken 13 times.
✓ Branch 1 taken 806 times.
|
819 | if (ret) { |
279 | 13 | return ret; | |
280 | } | ||
281 |
4/4✓ Branch 0 taken 754 times.
✓ Branch 1 taken 52 times.
✓ Branch 2 taken 130 times.
✓ Branch 3 taken 624 times.
|
806 | if (entry.e_tag == ACL_GROUP || entry.e_tag == ACL_USER) { |
282 | 182 | o_equiv_mode = false; | |
283 | } | ||
284 |
1/2✓ Branch 1 taken 806 times.
✗ Branch 2 not taken.
|
806 | entries.push_back(entry); |
285 | } | ||
286 | |||
287 | // sort entries as libacl does, to be able to use it in testing as a reference | ||
288 |
1/2✓ Branch 3 taken 273 times.
✗ Branch 4 not taken.
|
273 | sort(entries.begin(), entries.end()); |
289 | |||
290 | // reject what acl_valid() rejects, to be able to use it in testing | ||
291 |
2/2✓ Branch 1 taken 130 times.
✓ Branch 2 taken 143 times.
|
273 | if (!acl_valid_builtin(entries)) { |
292 | 130 | return EINVAL; | |
293 | } | ||
294 | |||
295 | // if nothing but usual u,g,o bits, don't produce a binary. Mimicking libacl. | ||
296 |
2/2✓ Branch 0 taken 78 times.
✓ Branch 1 taken 65 times.
|
143 | if (o_equiv_mode) { |
297 | 78 | o_binary_acl = NULL; | |
298 | 78 | o_size = 0; | |
299 | 78 | return 0; | |
300 | } | ||
301 | |||
302 | // get one big buffer with all the entries in the "on-disk" xattr format | ||
303 | 65 | size_t const acl_entry_count = entries.size(); | |
304 | 65 | size_t const buf_size = sizeof(acl_ea_header) | |
305 | 65 | + (acl_entry_count * sizeof(acl_ea_entry)); | |
306 | 65 | char *buf = static_cast<char *>(malloc(buf_size)); | |
307 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
|
65 | if (!buf) { |
308 | ✗ | return ENOMEM; | |
309 | } | ||
310 | 65 | acl_ea_header *header = reinterpret_cast<acl_ea_header *>(buf); | |
311 | 65 | header->a_version = htole32(ACL_EA_VERSION); | |
312 | 65 | acl_ea_entry *ext_entry = reinterpret_cast<acl_ea_entry *>(header + 1); | |
313 |
2/2✓ Branch 3 taken 377 times.
✓ Branch 4 taken 65 times.
|
442 | for (auto entry_it = entries.begin(); entry_it != entries.end(); ++entry_it) { |
314 | 377 | *ext_entry = *entry_it; | |
315 | 377 | ext_entry += 1; | |
316 | } | ||
317 | |||
318 | 65 | o_binary_acl = buf; | |
319 | 65 | o_size = buf_size; | |
320 | 65 | return 0; | |
321 | 286 | } | |
322 | |||
323 | #ifdef COMPARE_TO_LIBACL | ||
324 | int acl_from_text_to_xattr_value_libacl(const string textual_acl, | ||
325 | char *&o_binary_acl, size_t &o_size, | ||
326 | bool &o_equiv_mode) { | ||
327 | acl_t acl = acl_from_text( | ||
328 | textual_acl.c_str()); // Convert ACL string to acl_t object | ||
329 | if (!acl) { | ||
330 | return EINVAL; | ||
331 | } | ||
332 | if (acl_valid(acl) != 0) { | ||
333 | acl_free(acl); | ||
334 | return EINVAL; | ||
335 | } | ||
336 | |||
337 | // check if the ACL string contains more than the synthetic ACLs | ||
338 | int equiv = acl_equiv_mode(acl, NULL); | ||
339 | assert(equiv != -1); | ||
340 | |||
341 | o_equiv_mode = equiv == 0; | ||
342 | if (!o_equiv_mode) { | ||
343 | o_binary_acl = (char *)acl_to_xattr(acl, &o_size); | ||
344 | } else { | ||
345 | o_binary_acl = NULL; | ||
346 | o_size = 0; | ||
347 | } | ||
348 | acl_free(acl); | ||
349 | return 0; | ||
350 | } | ||
351 | |||
352 | int acl_from_text_to_xattr_value_both_impl(const string textual_acl, | ||
353 | char *&o_binary_acl, size_t &o_size, | ||
354 | bool &o_equiv_mode) { | ||
355 | struct impl_result { | ||
356 | int ret; | ||
357 | size_t binary_size; | ||
358 | char *binary_acl; | ||
359 | bool equiv_mode; | ||
360 | } b, l; | ||
361 | b.ret = acl_from_text_to_xattr_value(textual_acl, b.binary_acl, b.binary_size, | ||
362 | b.equiv_mode); | ||
363 | l.ret = acl_from_text_to_xattr_value_libacl(textual_acl, l.binary_acl, | ||
364 | l.binary_size, l.equiv_mode); | ||
365 | assert(b.ret == l.ret); | ||
366 | if (!l.ret) { | ||
367 | assert(b.binary_size == l.binary_size); | ||
368 | assert(0 == memcmp(b.binary_acl, l.binary_acl, b.binary_size)); | ||
369 | assert(b.equiv_mode == l.equiv_mode); | ||
370 | free(l.binary_acl); | ||
371 | } | ||
372 | o_binary_acl = b.binary_acl; | ||
373 | o_size = b.binary_size; | ||
374 | o_equiv_mode = b.equiv_mode; | ||
375 | return b.ret; | ||
376 | } | ||
377 | #endif // COMPARE_TO_LIBACL | ||
378 |