Skip to content

Authentication and authorization

clsADUserServices declares Windows authentication in Web.config but ships no <authorization> element, and the release pipeline enables IIS anonymous authentication for both deployment variants — so the effective barrier in front of the service is not the Windows identity but a single application-layer, programmatic, machine-level (network-origin) IP allow-list. That allow-list gates 8 of the 19 published operations; the remaining 11, including several that disclose Active Directory attribute and group data, carry no application-layer check at all.

Authentication

Aspect Finding
Mechanism Windows/NTLM declared via <authentication mode="Windows"/>, but IIS anonymous authentication is enabled for both deployment variants by the release pipeline
Established by Web.config declaration; effective enforcement owned by IIS site configuration and the external deploy template
<authentication> element mode="Windows"
Identity type Not determined from source — no User.Identity, WindowsIdentity, or IsInRole usage exists anywhere in the codebehind
Session state Not used — every <WebMethod()> is declared bare, so EnableSession defaults to False on all 19 operations
Anonymous access Allowed — IISAnonymousAuthInternal: true and IISAnonymousAuthExternal: true in pipelines\release.yml for both variants

Web.config declares <authentication mode="Windows"/> with no <authorization>, <identity impersonate>, or <location> element anywhere in the file. The release pipeline enables anonymous authentication for both the internal and external deployment variants, and never supplies IISWindowsAuthInternal/IISWindowsAuthExternal to the external deploy template, so the effective IIS Windows-authentication state is not determinable from this repository.** In combination, no caller is required to present a Windows identity to reach any operation; the only application-layer control is the IP allow-list documented below. AddUser.inPassword, ChangeUserPassword.inNewPassword, and ChangeUserPasswordOrGetException.inNewPassword accept a plaintext account password as a SOAP string parameter; these travel in the request body and their protection depends entirely on whatever transport binding fronts the deployed endpoint, which is not evidenced in this repository.**

Authorization enforcement

Mechanism Scope Declared in Effect
Programmatic check (CheckAccess().HasAccess) Operation src\ADUserServices\clsADUserServices.asmx.vb Grants access to a request that is local to the server, or whose observed source address exactly matches a configured allow-list entry; applied to 8 mutating/password operations
None in code 11 read operations — Every reachable request invokes the operation; access relies entirely on IIS anonymous authentication being disabled by the external deploy template, or on network placement, neither of which is confirmed by this repository

This is a network-origin (IP) allow-list, not a role check, a claim check, or a SAIF role-model integration. It carries no notion of which user or calling application is acting, so no operation can be constrained to a subset of accounts, groups, or attributes.

Access requirements by operation

Operation Requirement Enforced by On failure
HelloWorld None in code — Call proceeds; cannot fail
AddUser IP allow-list or local request CheckAccess().HasAccess, clsADUserServices.asmx.vb:105 AccessDenied sentinel
UnlockAccount IP allow-list or local request CheckAccess().HasAccess, clsADUserServices.asmx.vb:202 AccessDenied sentinel
AddUserToGroup IP allow-list or local request CheckAccess().HasAccess, clsADUserServices.asmx.vb:234 AccessDenied sentinel
DeleteUser IP allow-list or local request CheckAccess().HasAccess, clsADUserServices.asmx.vb:273 AccessDenied sentinel
DeleteUserFromGroup IP allow-list or local request CheckAccess().HasAccess, clsADUserServices.asmx.vb:302 AccessDenied sentinel
DoesUserExist None in code — Call proceeds
DoesUserHaveResource None in code — Call proceeds
ChangeUserAccount IP allow-list or local request CheckAccess().HasAccess, clsADUserServices.asmx.vb:395 AccessDenied sentinel
ChangeUserPassword IP allow-list or local request CheckAccess().HasAccess, clsADUserServices.asmx.vb:474 AccessDenied sentinel
ChangeUserPasswordOrGetException IP allow-list or local request CheckAccess().HasAccess, clsADUserServices.asmx.vb:495 AccessDenied sentinel
GetUserEmail None in code — Call proceeds; discloses mail
GetUserGroups None in code — Call proceeds; discloses transitive group membership
GetUserResources None in code — Call proceeds; discloses filtered resource-group membership
GetUserFAX None in code — Call proceeds; discloses facsimileTelephoneNumber
GetUserLastLogon None in code — Call proceeds; discloses last-logon timestamp
GetUserPrinter None in code — Call proceeds; discloses extensionAttribute2
IsUserInGroup None in code — Call proceeds; discloses direct group membership
CheckAccess None in code — it is itself the access-control primitive — Call proceeds; discloses the caller's observed remote address and the allow-list decision

Service account credentials

Credential Purpose Supplied to Retrieved from Notes
IIS application-pool identity Authenticates the service process to Active Directory for every directory read and write Every mutating operation, executed as the app-pool identity Azure DevOps variable group Architecture.ServiceAccounts.IIS (IISAppPoolUserNonProd/IISAppPoolPasswordNonProd, IISAppPoolUserProd/IISAppPoolPasswordProd) The same account variables are used for non-prod and prod (SEC-07, MEDIUM for identity reuse; the externalization mechanism itself is correct). No PrincipalContext in source is constructed with explicit credentials, so every AD action is attributed to this one shared account with no requester correlation

Secrets handling

Secret Location Retrieved by Consumed by Notes
Default Active Directory account password Embedded literal in source (src\ADUserServices\clsADUserServices.asmx.vb, AddUser, the If inPassword = String.Empty Then branch, within lines 118–122) Direct assignment when inPassword is empty clsADUserServices.AddUser CRITICAL (SEC-05). The account is created .Enabled = True. The literal value is not reproduced here or anywhere in this documentation set — see Tech debt

Security rules

SEC-SVC-001 A local request is always approved

Field Value
Statement Any request the ASP.NET runtime considers local to the server is unconditionally granted full access to the mutating surface
Trigger CheckAccess evaluation, ahead of and independent of the allow-list
Enforcement If (HttpContext.Current.Request.IsLocal) Then response.HasAccess = True — clsADUserServices.asmx.vb lines 762–765
Applies to CheckAccess and the eight gated operations
Outcome HasAccess = True, Local = True. Restated as BR-02. Any other process, site, or application co-hosted on the same server obtains the full directory-administration surface (SEC-11, MEDIUM)
Source clsADUserServices.asmx.vb:762-765

SEC-SVC-002 An allow-listed source address is approved

Field Value
Statement A request whose observed source address exactly (case-sensitive, untrimmed) matches a configured allow-list entry is granted access
Trigger CheckAccess evaluation
Enforcement For Each ip In GetAllowedIPs(), comparing each entry to GetIncomingIp(); on a match, sets response.HasAccess = True and response.EnvironmentApi = True — lines 767–772; comparison is raw string equality with no CIDR, parse, or trim (SEC-10, MEDIUM)
Applies to CheckAccess and the eight gated operations
Outcome HasAccess = True, EnvironmentApi = True. Restated as BR-03
Source clsADUserServices.asmx.vb:767-772

SEC-SVC-003 The incoming-IP source prefers a spoofable header

Field Value
Statement The address compared against the allow-list is taken from the HTTP_X_FORWARDED_FOR server variable, falling back to REMOTE_ADDR only when that header is null or empty, with no trusted-proxy validation
Trigger Every CheckAccess evaluation
Enforcement GetIncomingIp — lines 798–806
Applies to CheckAccess and the eight gated operations
Outcome The chosen value feeds SEC-SVC-002; a caller able to set the header can present an arbitrary source address (SEC-03, HIGH). Restated as BR-04
Source clsADUserServices.asmx.vb:798-806

SEC-SVC-004 A denied request returns a normal response, never a fault

Field Value
Statement A caller denied by CheckAccess receives a normal SOAP response carrying a failure sentinel, not a SOAP fault
Trigger Denial under SEC-SVC-001/SEC-SVC-002
Enforcement AccessDenied — lines 781–789
Applies to the eight gated operations
Outcome ADServiceResponse{Response=Failure, Message="Access Denied"}. Restated as BR-05
Source clsADUserServices.asmx.vb:781-789

SEC-SVC-005 CheckAccess itself is published and unauthenticated

Field Value
Statement The allow-list decision primitive is itself a published, unauthenticated operation that discloses the caller's observed remote address and whether it was accepted
Trigger Any call to CheckAccess
Enforcement No check precedes the body of CheckAccess — lines 752–776
Applies to CheckAccess
Outcome HasAccessResponse{HasAccess, Local, EnvironmentApi, RemoteAddress} returned to any caller, providing an oracle for probing the allow-list (SEC-04, MEDIUM)
Source clsADUserServices.asmx.vb:752-776

Notes

  • Mark inferred statements with * and unverifiable statements with **. Do not use bold inline confidence labels.

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.vb
  • src\ADUserServices\Web.config
  • pipelines\release.yml
  • .saifdocgen\state\runs\20260922-101326-aduserservices\analysis\aduserservices-analysis.md
  • .saifdocgen\state\runs\20260922-101326-aduserservices\analysis\aduserservices-security-analysis.md