]>
Commit | Line | Data |
---|---|---|
b1ab9ed8 | 1 | /* |
d8f41ccd | 2 | * Copyright (c) 2002-2004,2011-2012,2014 Apple Inc. All Rights Reserved. |
b1ab9ed8 A |
3 | * |
4 | * @APPLE_LICENSE_HEADER_START@ | |
5 | * | |
6 | * This file contains Original Code and/or Modifications of Original Code | |
7 | * as defined in and that are subject to the Apple Public Source License | |
8 | * Version 2.0 (the 'License'). You may not use this file except in | |
9 | * compliance with the License. Please obtain a copy of the License at | |
10 | * http://www.opensource.apple.com/apsl/ and read it before using this | |
11 | * file. | |
12 | * | |
13 | * The Original Code and all software distributed under the License are | |
14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER | |
15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, | |
16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. | |
18 | * Please see the License for the specific language governing rights and | |
19 | * limitations under the License. | |
20 | * | |
21 | * @APPLE_LICENSE_HEADER_END@ | |
22 | */ | |
23 | ||
24 | // | |
25 | // ACL.cpp | |
26 | // | |
27 | #include <security_keychain/ACL.h> | |
28 | #include <security_keychain/SecCFTypes.h> | |
29 | #include <security_utilities/osxcode.h> | |
30 | #include <security_utilities/trackingallocator.h> | |
31 | #include <security_cdsa_utilities/walkers.h> | |
32 | #include <security_keychain/TrustedApplication.h> | |
33 | #include <Security/SecTrustedApplication.h> | |
b54c578e | 34 | #include <Security/SecRandom.h> |
b1ab9ed8 A |
35 | #include <memory> |
36 | ||
37 | ||
38 | using namespace KeychainCore; | |
39 | using namespace DataWalkers; | |
40 | ||
41 | ||
42 | // | |
43 | // The default form of a prompt selector | |
44 | // | |
45 | const CSSM_ACL_KEYCHAIN_PROMPT_SELECTOR ACL::defaultSelector = { | |
46 | CSSM_ACL_KEYCHAIN_PROMPT_CURRENT_VERSION, 0 | |
47 | }; | |
48 | ||
49 | ||
50 | // | |
51 | // ACL static constants | |
52 | // | |
53 | const CSSM_ACL_HANDLE ACL::ownerHandle; | |
54 | ||
55 | ||
56 | // | |
57 | // Create an ACL object from the result of a CSSM ACL query | |
58 | // | |
e3d460c9 A |
59 | ACL::ACL(const AclEntryInfo &info, Allocator &alloc) |
60 | : allocator(alloc), mState(unchanged), mSubjectForm(NULL), mIntegrity(alloc), mMutex(Mutex::recursive) | |
b1ab9ed8 A |
61 | { |
62 | // parse the subject | |
63 | parse(info.proto().subject()); | |
e3d460c9 | 64 | |
b1ab9ed8 A |
65 | // fill in AclEntryInfo layer information |
66 | const AclEntryPrototype &proto = info.proto(); | |
67 | mAuthorizations = proto.authorization(); | |
68 | mDelegate = proto.delegate(); | |
69 | mEntryTag = proto.s_tag(); | |
70 | ||
71 | // take CSSM entry handle from info layer | |
72 | mCssmHandle = info.handle(); | |
73 | } | |
74 | ||
e3d460c9 A |
75 | |
76 | ACL::ACL(const AclOwnerPrototype &owner, Allocator &alloc) | |
77 | : allocator(alloc), mState(unchanged), mSubjectForm(NULL), mIntegrity(alloc), mMutex(Mutex::recursive) | |
b1ab9ed8 A |
78 | { |
79 | // parse subject | |
80 | parse(owner.subject()); | |
81 | ||
82 | // for an owner "entry", the next-layer information is fixed (and fake) | |
83 | mAuthorizations.insert(CSSM_ACL_AUTHORIZATION_CHANGE_ACL); | |
84 | mDelegate = owner.delegate(); | |
85 | mEntryTag[0] = '\0'; | |
86 | ||
87 | // use fixed (fake) entry handle | |
88 | mCssmHandle = ownerHandle; | |
89 | } | |
90 | ||
91 | ||
92 | // | |
93 | // Create a new ACL that authorizes anyone to do anything. | |
94 | // This constructor produces a "pure" ANY ACL, without descriptor or selector. | |
95 | // To generate a "standard" form of ANY, use the appListForm constructor below, | |
96 | // then change its form to allowAnyForm. | |
97 | // | |
e3d460c9 A |
98 | ACL::ACL(Allocator &alloc) |
99 | : allocator(alloc), mSubjectForm(NULL), mIntegrity(alloc), mMutex(Mutex::recursive) | |
b1ab9ed8 A |
100 | { |
101 | mState = inserted; // new | |
102 | mForm = allowAllForm; // everybody | |
103 | mAuthorizations.insert(CSSM_ACL_AUTHORIZATION_ANY); // anything | |
104 | mDelegate = false; | |
105 | ||
106 | //mPromptDescription stays empty | |
107 | mPromptSelector = defaultSelector; | |
108 | ||
109 | // randomize the CSSM handle | |
b54c578e | 110 | MacOSError::check(SecRandomCopyBytes(kSecRandomDefault, sizeof(mCssmHandle), (void *)mCssmHandle)); |
b1ab9ed8 A |
111 | } |
112 | ||
113 | ||
114 | // | |
115 | // Create a new ACL in standard form. | |
116 | // As created, it authorizes all activities. | |
117 | // | |
e3d460c9 | 118 | ACL::ACL(string description, const CSSM_ACL_KEYCHAIN_PROMPT_SELECTOR &promptSelector, |
b1ab9ed8 | 119 | Allocator &alloc) |
e3d460c9 | 120 | : allocator(alloc), mSubjectForm(NULL), mIntegrity(alloc), mMutex(Mutex::recursive) |
b1ab9ed8 A |
121 | { |
122 | mState = inserted; // new | |
123 | mForm = appListForm; | |
124 | mAuthorizations.insert(CSSM_ACL_AUTHORIZATION_ANY); // anything | |
125 | mDelegate = false; | |
126 | ||
127 | mPromptDescription = description; | |
128 | mPromptSelector = promptSelector; | |
129 | ||
130 | // randomize the CSSM handle | |
b54c578e | 131 | MacOSError::check(SecRandomCopyBytes(kSecRandomDefault, sizeof(mCssmHandle), &mCssmHandle)); |
b1ab9ed8 A |
132 | } |
133 | ||
134 | ||
e3d460c9 A |
135 | // |
136 | // Create an "integrity" ACL | |
137 | // | |
138 | ACL::ACL(const CssmData &digest, Allocator &alloc) | |
139 | : allocator(alloc), mSubjectForm(NULL), mIntegrity(alloc, digest), mMutex(Mutex::recursive) | |
140 | { | |
141 | mState = inserted; // new | |
142 | mForm = integrityForm; | |
143 | mAuthorizations.insert(CSSM_ACL_AUTHORIZATION_INTEGRITY); | |
144 | mEntryTag = CSSM_APPLE_ACL_TAG_INTEGRITY; | |
145 | mDelegate = false; | |
146 | ||
147 | //mPromptDescription stays empty | |
148 | //mPromptSelector stays empty | |
149 | ||
150 | // randomize the CSSM handle | |
b54c578e | 151 | MacOSError::check(SecRandomCopyBytes(kSecRandomDefault, sizeof(mCssmHandle), &mCssmHandle)); |
e3d460c9 A |
152 | } |
153 | ||
154 | ||
b1ab9ed8 A |
155 | // |
156 | // Destroy an ACL | |
157 | // | |
158 | ACL::~ACL() | |
159 | { | |
160 | // release subject form (if any) | |
161 | chunkFree(mSubjectForm, allocator); | |
162 | } | |
163 | ||
164 | ||
165 | // | |
166 | // Does this ACL authorize a particular right? | |
167 | // | |
168 | bool ACL::authorizes(AclAuthorization right) | |
169 | { | |
170 | StLock<Mutex>_(mMutex); | |
171 | return mAuthorizations.find(right) != mAuthorizations.end() | |
172 | || mAuthorizations.find(CSSM_ACL_AUTHORIZATION_ANY) != mAuthorizations.end() | |
173 | || mAuthorizations.empty(); | |
174 | } | |
175 | ||
e3d460c9 A |
176 | // |
177 | // Does this ACL have a specific authorization for a particular right? | |
178 | // | |
179 | bool ACL::authorizesSpecifically(AclAuthorization right) | |
180 | { | |
181 | StLock<Mutex>_(mMutex); | |
182 | return mAuthorizations.find(right) != mAuthorizations.end(); | |
183 | } | |
184 | ||
185 | void ACL::setIntegrity(const CssmData& digest) { | |
186 | if(mForm != integrityForm) { | |
fa7225c8 | 187 | secnotice("integrity", "acl has incorrect form: %d", mForm); |
e3d460c9 A |
188 | CssmError::throwMe(CSSMERR_CSP_INVALID_ACL_SUBJECT_VALUE); |
189 | } | |
190 | ||
191 | mIntegrity = digest; | |
192 | modify(); | |
193 | } | |
194 | ||
195 | const CssmData& ACL::integrity() { | |
196 | return mIntegrity.get(); | |
197 | } | |
b1ab9ed8 A |
198 | |
199 | // | |
200 | // Add an application to the trusted-app list of this ACL. | |
201 | // Will fail unless this is a standard "simple" form ACL. | |
202 | // | |
203 | void ACL::addApplication(TrustedApplication *app) | |
204 | { | |
205 | StLock<Mutex>_(mMutex); | |
206 | switch (mForm) { | |
207 | case appListForm: // simple... | |
208 | mAppList.push_back(app); | |
209 | modify(); | |
210 | break; | |
211 | case allowAllForm: // hmm... | |
212 | if (!mPromptDescription.empty()) { | |
213 | // verbose "any" form (has description, "any" override) | |
214 | mAppList.push_back(app); | |
215 | modify(); | |
216 | break; | |
217 | } | |
218 | // pure "any" form without description. Cannot convert to appListForm | |
219 | default: | |
220 | MacOSError::throwMe(errSecACLNotSimple); | |
221 | } | |
222 | } | |
223 | ||
224 | ||
225 | // | |
226 | // Mark an ACL as modified. | |
227 | // | |
228 | void ACL::modify() | |
229 | { | |
230 | StLock<Mutex>_(mMutex); | |
231 | if (mState == unchanged) { | |
fa7225c8 | 232 | secinfo("SecAccess", "ACL %p marked modified", this); |
b1ab9ed8 A |
233 | mState = modified; |
234 | } | |
235 | } | |
236 | ||
237 | ||
238 | // | |
239 | // Mark an ACL as "removed" | |
240 | // Removed ACLs have no valid contents (they are invalid on their face). | |
241 | // When "updated" to the originating item, they will cause the corresponding | |
242 | // ACL entry to be deleted. Otherwise, they are irrelevant. | |
243 | // Note: Removing an ACL does not actually remove it from its Access's map. | |
244 | // | |
245 | void ACL::remove() | |
246 | { | |
247 | StLock<Mutex>_(mMutex); | |
248 | mAppList.clear(); | |
249 | mForm = invalidForm; | |
fa7225c8 | 250 | secinfo("SecAccess", "ACL %p marked deleted", this); |
b1ab9ed8 A |
251 | mState = deleted; |
252 | } | |
253 | ||
254 | ||
255 | // | |
256 | // Produce CSSM-layer form (ACL prototype) copies of our content. | |
257 | // Note that the result is chunk-allocated, and becomes owned by the caller. | |
258 | // | |
259 | void ACL::copyAclEntry(AclEntryPrototype &proto, Allocator &alloc) | |
260 | { | |
261 | StLock<Mutex>_(mMutex); | |
262 | proto.clearPod(); // preset | |
263 | ||
264 | // carefully copy the subject | |
265 | makeSubject(); | |
266 | assert(mSubjectForm); | |
267 | proto = AclEntryPrototype(*mSubjectForm, mDelegate); // shares subject | |
268 | ChunkCopyWalker w(alloc); | |
269 | walk(w, proto.subject()); // copy subject in-place | |
270 | ||
271 | // the rest of a prototype | |
272 | proto.tag(mEntryTag); | |
273 | AuthorizationGroup tags(mAuthorizations, allocator); | |
274 | proto.authorization() = tags; | |
275 | } | |
276 | ||
277 | void ACL::copyAclOwner(AclOwnerPrototype &proto, Allocator &alloc) | |
278 | { | |
279 | StLock<Mutex>_(mMutex); | |
280 | proto.clearPod(); | |
281 | ||
282 | makeSubject(); | |
283 | assert(mSubjectForm); | |
284 | proto = AclOwnerPrototype(*mSubjectForm, mDelegate); // shares subject | |
285 | ChunkCopyWalker w(alloc); | |
286 | walk(w, proto.subject()); // copy subject in-place | |
287 | } | |
288 | ||
289 | ||
290 | // | |
291 | // (Re)place this ACL's setting into the AclBearer specified. | |
292 | // If update, assume this is an update operation and the ACL was | |
293 | // originally derived from this object; specifically, assume the | |
294 | // CSSM handle is valid. If not update, assume this is a different | |
295 | // object that has no related ACL entry (yet). | |
296 | // | |
297 | void ACL::setAccess(AclBearer &target, bool update, | |
298 | const AccessCredentials *cred) | |
299 | { | |
300 | StLock<Mutex>_(mMutex); | |
301 | // determine what action we need to perform | |
302 | State action = state(); | |
303 | if (!update) | |
304 | action = (action == deleted) ? unchanged : inserted; | |
305 | ||
306 | // the owner acl (pseudo) "entry" is a special case | |
307 | if (isOwner()) { | |
308 | switch (action) { | |
309 | case unchanged: | |
fa7225c8 | 310 | secinfo("SecAccess", "ACL %p owner unchanged", this); |
b1ab9ed8 A |
311 | return; |
312 | case inserted: // means modify the initial owner | |
313 | case modified: | |
314 | { | |
fa7225c8 | 315 | secinfo("SecAccess", "ACL %p owner modified", this); |
b1ab9ed8 A |
316 | makeSubject(); |
317 | assert(mSubjectForm); | |
318 | AclOwnerPrototype proto(*mSubjectForm, mDelegate); | |
319 | target.changeOwner(proto, cred); | |
320 | return; | |
321 | } | |
322 | default: | |
323 | assert(false); | |
324 | return; | |
325 | } | |
326 | } | |
327 | ||
328 | // simple cases | |
329 | switch (action) { | |
330 | case unchanged: // ignore | |
fa7225c8 | 331 | secinfo("SecAccess", "ACL %p handle 0x%lx unchanged", this, entryHandle()); |
b1ab9ed8 A |
332 | return; |
333 | case deleted: // delete | |
fa7225c8 | 334 | secinfo("SecAccess", "ACL %p handle 0x%lx deleted", this, entryHandle()); |
b1ab9ed8 A |
335 | target.deleteAcl(entryHandle(), cred); |
336 | return; | |
337 | default: | |
338 | break; | |
339 | } | |
340 | ||
341 | // build the byzantine data structures that CSSM loves so much | |
342 | makeSubject(); | |
343 | assert(mSubjectForm); | |
344 | AclEntryPrototype proto(*mSubjectForm, mDelegate); | |
345 | proto.tag(mEntryTag); | |
346 | AutoAuthorizationGroup tags(mAuthorizations, allocator); | |
347 | proto.authorization() = tags; | |
348 | AclEntryInput input(proto); | |
349 | switch (action) { | |
350 | case inserted: // insert | |
fa7225c8 | 351 | secinfo("SecAccess", "ACL %p inserted", this); |
b1ab9ed8 | 352 | target.addAcl(input, cred); |
e3d460c9 | 353 | mState = unchanged; |
b1ab9ed8 A |
354 | break; |
355 | case modified: // update | |
fa7225c8 | 356 | secinfo("SecAccess", "ACL %p handle 0x%lx modified", this, entryHandle()); |
b1ab9ed8 | 357 | target.changeAcl(entryHandle(), input, cred); |
e3d460c9 | 358 | mState = unchanged; |
b1ab9ed8 A |
359 | break; |
360 | default: | |
361 | assert(false); | |
362 | } | |
363 | } | |
364 | ||
365 | ||
366 | // | |
367 | // Parse an AclEntryPrototype (presumably from a CSSM "Get" ACL operation | |
368 | // into internal form. | |
369 | // | |
370 | void ACL::parse(const TypedList &subject) | |
371 | { | |
372 | StLock<Mutex>_(mMutex); | |
373 | try { | |
374 | switch (subject.type()) { | |
375 | case CSSM_ACL_SUBJECT_TYPE_ANY: | |
376 | // subsume an "any" as a standard form | |
377 | mForm = allowAllForm; | |
fa7225c8 | 378 | secinfo("SecAccess", "parsed an allowAllForm (%d) (%d)", subject.type(), mForm); |
b1ab9ed8 A |
379 | return; |
380 | case CSSM_ACL_SUBJECT_TYPE_KEYCHAIN_PROMPT: | |
381 | // pure keychain prompt - interpret as applist form with no apps | |
382 | parsePrompt(subject); | |
383 | mForm = appListForm; | |
fa7225c8 | 384 | secinfo("SecAccess", "parsed a Keychain Prompt (%d) as an appListForm (%d)", subject.type(), mForm); |
b1ab9ed8 A |
385 | return; |
386 | case CSSM_ACL_SUBJECT_TYPE_THRESHOLD: | |
387 | { | |
388 | // app-list format: THRESHOLD(1, n): sign(1), ..., sign(n), PROMPT | |
389 | if (subject[1] != 1) | |
390 | throw ParseError(); | |
391 | uint32 count = subject[2]; | |
392 | ||
393 | // parse final (PROMPT) element | |
394 | TypedList &end = subject[count + 2]; // last choice | |
395 | if (end.type() != CSSM_ACL_SUBJECT_TYPE_KEYCHAIN_PROMPT) | |
396 | throw ParseError(); // not PROMPT at end | |
397 | parsePrompt(end); | |
398 | ||
399 | // check for leading ANY | |
400 | TypedList &first = subject[3]; | |
401 | if (first.type() == CSSM_ACL_SUBJECT_TYPE_ANY) { | |
402 | mForm = allowAllForm; | |
fa7225c8 | 403 | secinfo("SecAccess", "parsed a Threshhold (%d) as an allowAllForm (%d)", subject.type(), mForm); |
b1ab9ed8 A |
404 | return; |
405 | } | |
406 | ||
407 | // parse other (code signing) elements | |
e3d460c9 A |
408 | for (uint32 n = 0; n < count - 1; n++) { |
409 | mAppList.push_back(new TrustedApplication(TypedList(subject[n + 3].list()))); | |
fa7225c8 | 410 | secinfo("SecAccess", "found an application: %s", mAppList.back()->path()); |
e3d460c9 | 411 | } |
b1ab9ed8 A |
412 | } |
413 | mForm = appListForm; | |
fa7225c8 | 414 | secinfo("SecAccess", "parsed a Threshhold (%d) as an appListForm (%d)", subject.type(), mForm); |
b1ab9ed8 | 415 | return; |
e3d460c9 A |
416 | case CSSM_ACL_SUBJECT_TYPE_PARTITION: |
417 | mForm = integrityForm; | |
418 | mIntegrity.copy(subject.last()->data()); | |
fa7225c8 | 419 | secinfo("SecAccess", "parsed a Partition (%d) as an integrityForm (%d)", subject.type(), mForm); |
e3d460c9 A |
420 | return; |
421 | default: | |
fa7225c8 | 422 | secinfo("SecAccess", "didn't find a type for %d, marking custom (%d)", subject.type(), mForm); |
b1ab9ed8 A |
423 | mForm = customForm; |
424 | mSubjectForm = chunkCopy(&subject); | |
425 | return; | |
426 | } | |
427 | } catch (const ParseError &) { | |
fa7225c8 | 428 | secinfo("SecAccess", "acl compile failed for type (%d); marking custom", subject.type()); |
b1ab9ed8 A |
429 | mForm = customForm; |
430 | mSubjectForm = chunkCopy(&subject); | |
431 | mAppList.clear(); | |
432 | } | |
433 | } | |
434 | ||
435 | void ACL::parsePrompt(const TypedList &subject) | |
436 | { | |
437 | StLock<Mutex>_(mMutex); | |
438 | assert(subject.length() == 3); | |
439 | mPromptSelector = | |
440 | *subject[1].data().interpretedAs<CSSM_ACL_KEYCHAIN_PROMPT_SELECTOR>(CSSM_ERRCODE_INVALID_ACL_SUBJECT_VALUE); | |
441 | mPromptDescription = subject[2].toString(); | |
442 | } | |
443 | ||
444 | ||
445 | // | |
446 | // Take this ACL and produce its meaning as a CSSM ACL subject in mSubjectForm | |
447 | // | |
448 | void ACL::makeSubject() | |
449 | { | |
450 | StLock<Mutex>_(mMutex); | |
451 | switch (form()) { | |
452 | case allowAllForm: | |
453 | chunkFree(mSubjectForm, allocator); // release previous | |
454 | if (mPromptDescription.empty()) { | |
455 | // no description -> pure ANY | |
456 | mSubjectForm = new(allocator) TypedList(allocator, CSSM_ACL_SUBJECT_TYPE_ANY); | |
457 | } else { | |
458 | // have description -> threshold(1 of 2) of { ANY, PROMPT } | |
459 | mSubjectForm = new(allocator) TypedList(allocator, CSSM_ACL_SUBJECT_TYPE_THRESHOLD, | |
460 | new(allocator) ListElement(1), | |
461 | new(allocator) ListElement(2)); | |
462 | *mSubjectForm += new(allocator) ListElement(TypedList(allocator, CSSM_ACL_SUBJECT_TYPE_ANY)); | |
463 | TypedList prompt(allocator, CSSM_ACL_SUBJECT_TYPE_KEYCHAIN_PROMPT, | |
464 | new(allocator) ListElement(allocator, CssmData::wrap(mPromptSelector)), | |
465 | new(allocator) ListElement(allocator, mPromptDescription)); | |
466 | *mSubjectForm += new(allocator) ListElement(prompt); | |
467 | } | |
fa7225c8 | 468 | secinfo("SecAccess", "made an allowAllForm (%d) into a subjectForm (%d)", mForm, mSubjectForm->type()); |
b1ab9ed8 A |
469 | return; |
470 | case appListForm: { | |
471 | // threshold(1 of n+1) of { app1, ..., appn, PROMPT } | |
472 | chunkFree(mSubjectForm, allocator); // release previous | |
427c49bc | 473 | uint32 appCount = (uint32)mAppList.size(); |
b1ab9ed8 A |
474 | mSubjectForm = new(allocator) TypedList(allocator, CSSM_ACL_SUBJECT_TYPE_THRESHOLD, |
475 | new(allocator) ListElement(1), | |
476 | new(allocator) ListElement(appCount + 1)); | |
477 | for (uint32 n = 0; n < appCount; n++) | |
478 | *mSubjectForm += | |
479 | new(allocator) ListElement(mAppList[n]->makeSubject(allocator)); | |
480 | TypedList prompt(allocator, CSSM_ACL_SUBJECT_TYPE_KEYCHAIN_PROMPT, | |
481 | new(allocator) ListElement(allocator, CssmData::wrap(mPromptSelector)), | |
482 | new(allocator) ListElement(allocator, mPromptDescription)); | |
483 | *mSubjectForm += new(allocator) ListElement(prompt); | |
484 | } | |
fa7225c8 | 485 | secinfo("SecAccess", "made an appListForm (%d) into a subjectForm (%d)", mForm, mSubjectForm->type()); |
b1ab9ed8 | 486 | return; |
e3d460c9 A |
487 | case integrityForm: |
488 | chunkFree(mSubjectForm, allocator); | |
489 | mSubjectForm = new(allocator) TypedList(allocator, CSSM_ACL_SUBJECT_TYPE_PARTITION, | |
490 | new(allocator) ListElement(allocator, mIntegrity)); | |
fa7225c8 | 491 | secinfo("SecAccess", "made an integrityForm (%d) into a subjectForm (%d)", mForm, mSubjectForm->type()); |
e3d460c9 | 492 | return; |
b1ab9ed8 A |
493 | case customForm: |
494 | assert(mSubjectForm); // already set; keep it | |
fa7225c8 | 495 | secinfo("SecAccess", "have a customForm (%d), already have a subjectForm (%d)", mForm, mSubjectForm->type()); |
b1ab9ed8 | 496 | return; |
e3d460c9 | 497 | |
b1ab9ed8 A |
498 | default: |
499 | assert(false); // unexpected | |
500 | } | |
501 | } |