Faults and error pipeline¶
clsADUserServices has no fault contract mechanism of its own — ASMX offers none — and no SoapException is ever constructed in source. Most operations catch a directory or password failure and return an ADServiceResponse sentinel instead of throwing; a smaller set of operations have no Try/Catch at all, so an escaping .NET exception reaches ASP.NET's default wrapping and surfaces as a SoapException. Several sentinel-returning operations catch and swallow the failure entirely, producing a response indistinguishable from success.
Fault surface summary¶
| Aspect | Finding |
|---|---|
| Fault contracts | Not applicable — ASMX has no fault contract mechanism |
| Exception wrapping | ASP.NET wraps an escaping exception in SoapException |
Custom SoapException throwing |
None — no SoapException is constructed anywhere in clsADUserServices.asmx.vb |
| Global handler | None — no Global.asax exists in the repository, so there is no Application_Error |
| Detail exposure | No <customErrors> element appears in src\ADUserServices\Web.config — the effective detail-exposure mode is a framework default and is not determined from source.** <compilation debug="true"> is present in the checked-in Web.config and is removed only by the Web.Release.config transform (see tech-debt.md TD-13) |
| Sentinel-value failures | 14 of 19 operations return a sentinel value on failure instead of throwing (see below) |
| Effect on callers | A caller cannot rely on a uniform failure signal: some operations fault, most return ADServiceResponse{Response=Failure} with ex.Message copied into Message, and a subset swallow the failure and return Success or an empty collection indistinguishable from a legitimate empty result |
Exception handling by operation¶
| Operation | Handling | What the caller receives | Logged |
|---|---|---|---|
HelloWorld |
No failure path exists — constant return | "Hello World" |
No |
AddUser |
Caught and converted to a return value inside two Try blocks; unhandled outside them |
ADServiceResponse{Failure} | SoapException (context/New UserPrincipal/property-assignment failures) |
No |
UnlockAccount |
Caught and converted — whole body in one Try/Catch |
ADServiceResponse{Failure, ex.Message} |
No |
AddUserToGroup |
Caught and converted | ADServiceResponse{UserAlreadyInGroup \| Failure, ex.Message} |
No |
DeleteUser |
Caught and converted; missing-user path falls through with no exception at all | ADServiceResponse{Success \| Failure, ex.Message} |
No |
DeleteUserFromGroup |
Caught and swallowed — authored comment 'gobble - user is pribably not in the group |
ADServiceResponse{Success} — indistinguishable from a genuine removal |
No |
DoesUserExist |
Caught and swallowed inside the shared GetUserPrincipal helper (H2), into m_LastMessage, never read |
False — indistinguishable from "user absent" |
No |
DoesUserHaveResource |
Unhandled — no Try/Catch around GetAuthorizationGroups() |
SoapException |
No |
ChangeUserAccount |
Caught and converted | ADServiceResponse{UserDoesNotExist \| Failure, ex.Message} |
No |
ChangeUserPassword |
Caught and converted — wraps the private ChangePassword helper (H6) |
ADServiceResponse{Failure, ex.Message} |
No |
ChangeUserPasswordOrGetException |
Unhandled — no Try/Catch around H6; deliberately faulting by design |
SoapException |
No |
GetUserEmail |
Caught and converted | ADServiceResponse{UserDoesNotExist \| Failure, ex.Message} |
No |
GetUserGroups |
Caught and swallowed — empty Catch block |
Empty ArrayList — indistinguishable from "no groups" |
No |
GetUserResources |
Caught and swallowed — empty Catch block |
Empty ArrayList — indistinguishable from "no matching groups" |
No |
GetUserFAX |
Caught and converted | ADServiceResponse{UserDoesNotExist \| Failure, ex.Message} |
No |
GetUserLastLogon |
Caught and converted | ADServiceResponse{UserDoesNotExist \| InfoNotAvailable \| Failure, ex.Message} |
No |
GetUserPrinter |
Caught and converted | ADServiceResponse{UserDoesNotExist \| Failure, ex.Message} |
No |
IsUserInGroup |
Caught and converted | ADServiceResponse{UserDoesNotExist \| GroupDoesNotExist \| Failure, ex.Message} |
No |
CheckAccess |
Unhandled — no Try/Catch around GetAllowedIPs() |
SoapException |
No |
No operation logs on failure — see Logging on failure below. The "caught and swallowed" rows are the most consequential findings on this page: the caller receives an apparently successful (or empty) response that is indistinguishable from a genuine result. Each is raised in tech-debt.md (DeleteUserFromGroup TD-20, GetUserGroups/GetUserResources TD-21, DoesUserExist TD-22).
Error pipeline diagram¶
flowchart TD
OP["Operation body raises<br>a .NET exception"] --> CATCH{"Caught in<br>the operation?"}
CATCH -->|"No — 4 operations"| WRAP["ASP.NET wraps it<br>in SoapException"]
CATCH -->|"Yes — 14 operations"| HANDLED{"Handling"}
HANDLED -->|"Convert to sentinel"| SENTINEL["ADServiceResponse<br>Failure plus ex.Message"]
HANDLED -->|"Swallow<br>DeleteUserFromGroup /<br>GetUserGroups / GetUserResources /<br>DoesUserExist"| SILENT["Response indistinguishable<br>from success or empty result"]
WRAP --> DETAIL{"customErrors mode"}
DETAIL -->|"Not declared in Web.config"| UNKNOWN["Detail exposure is a<br>framework default -<br>not determined from source"]
UNKNOWN --> CALLER["Caller receives SOAP fault"]
Custom SOAP faults¶
No operation constructs a SoapException anywhere in clsADUserServices.asmx.vb. Every fault a caller observes on this service is framework-generated from an unhandled .NET exception escaping one of the four operations listed in Exception handling by operation that carry no Try/Catch: AddUser (outside its two guarded blocks), DoesUserHaveResource, ChangeUserPasswordOrGetException, and CheckAccess.
Sentinel-value failure conventions¶
| Operation | Sentinel | Meaning | Ambiguity |
|---|---|---|---|
AddUser |
ADServiceResponse{Response=Failure} |
Access denied, existing user, or a caught save/commit failure | The existing-user code path (UserExists) is set then immediately overwritten to Failure, so a duplicate-user attempt is indistinguishable from any other failure (tech-debt.md TD-17) |
UnlockAccount |
Response=Success with free-text Message |
Unlocked, already-unlocked, or unlock-did-not-take-effect | The three outcomes are distinguished only in free text, not in Response |
DeleteUser |
Response=Success |
Deleted, or the user was never found | A missing user and a successful delete are indistinguishable; a directory-lookup failure inside GetUserPrincipal is also swallowed into this same Success |
DeleteUserFromGroup |
Response=Success |
Removed, was not a member, or a genuine directory failure | All three collapse to Success via an explicit swallow ('gobble) (tech-debt.md TD-20, HIGH) |
DoesUserExist |
False |
User absent, or directory lookup failed | Both collapse to False; the exception is captured in m_LastMessage, which no caller can read (tech-debt.md TD-22) |
GetUserGroups |
Empty ArrayList |
No groups, no such user, or directory failure | All three collapse to an empty list via an empty Catch block (tech-debt.md TD-21, HIGH) |
GetUserResources |
Empty ArrayList |
No matching resources, no such user, or directory failure | Same collapse as GetUserGroups (tech-debt.md TD-21, HIGH) |
ChangeUserAccount |
Response=Success |
A request that changes only inCompany/inInfo reports success but commits nothing, because those two writes never set the modified flag |
Indistinguishable from a genuine successful update (tech-debt.md TD-19, HIGH) |
All other sentinel-returning operations (AddUserToGroup, ChangeUserPassword, GetUserEmail, GetUserFAX, GetUserLastLogon, GetUserPrinter, IsUserInGroup) |
ADServiceResponse{Response=Failure, Message=ex.Message} |
Raw .NET exception text is flattened into free text | The distinction between a directory-connectivity failure and a business-rule rejection (for example a password-policy failure) is not preserved in Response, only in the free-text Message* |
Logging on failure¶
| Surface | Location | Written content | Failure of the logger itself |
|---|---|---|---|
| None | — | No logging framework is initialized or invoked anywhere in clsADUserServices.asmx.vb, despite the log4net 2.0.5 package reference in packages.config (referenced, never used — tech-debt.md TD-08). No Trace.WriteLine, EventLog write, or StreamWriter exists in source |
Not applicable |
No mutating operation writes an audit trail of any kind — see tech-debt.md SEC-09. m_LastMessage, the one field that captures directory-failure detail, is written by the three shared helpers (GetContext, GetUserPrincipal, GetGroupPrincipal) but is never read or returned by any operation (tech-debt.md TD-07).
Related documentation¶
- Service index
- Overview
- Architecture
- Activation and hosting
- Operations
- Operations and processes
- Business rules
- Authentication and authorization
- Configuration
- Dependencies
- Operational runbook
- Tech debt
Note: Statements marked with * were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts. Statements marked with ** could not be confirmed; treat as unknown until verified.
Sources¶
src\ADUserServices\clsADUserServices.asmx.vbsrc\ADUserServices\Web.configsrc\ADUserServices\Web.Release.config.saifdocgen\state\runs\20260922-101326-aduserservices\analysis\aduserservices-analysis.md