C-6: ConnectorRuntime enforces capability_mask per operation.
READ-only ACs cannot invoke MUTATE operations (wipe, lock, retire).
C-7: AC validated against database (exists, active, not expired)
before connector invocation.
C-9: Delegated AC capability bounded by delegator's capability.
C-10: Command counter uses atomic SQL increment with limit check.
M-23: expire_stale() uses same atomic SQL pattern.
H-1: Sensitive credential fields hidden from repr/logs via repr=False.
H-2: Stub backend requires ALLOW_STUB_CREDENTIALS=true to activate.
H-3: Kerberos backend raises CredentialResolutionError instead of
returning stub ticket.
H-4: Chronicle INTENT emitted before execution, RESULT after.
H-5: device_id validated as UUID before Graph API URL interpolation.
H-8: ConnectorRuntime enforces governance for all connector invocations.
Signed-off-by: Tyler King <tking@guildhouse.dev>
129 lines
6 KiB
Python
129 lines
6 KiB
Python
#------------------------------------------------------------------------------
|
|
#
|
|
# Copyright (c) Microsoft Corporation.
|
|
# All rights reserved.
|
|
#
|
|
# This code is licensed under the MIT License.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files(the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions :
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
#
|
|
#------------------------------------------------------------------------------
|
|
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
|
|
from .mex import Mex
|
|
from .wstrust_response import parse_response
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def send_request(
|
|
username, password, cloud_audience_urn, endpoint_address, soap_action, http_client,
|
|
**kwargs):
|
|
if not endpoint_address:
|
|
raise ValueError("WsTrust endpoint address can not be empty")
|
|
if soap_action is None:
|
|
if '/trust/2005/usernamemixed' in endpoint_address:
|
|
soap_action = Mex.ACTION_2005
|
|
elif '/trust/13/usernamemixed' in endpoint_address:
|
|
soap_action = Mex.ACTION_13
|
|
if soap_action not in (Mex.ACTION_13, Mex.ACTION_2005):
|
|
raise ValueError("Unsupported soap action: %s. "
|
|
"Contact your administrator to check your ADFS's MEX settings." % soap_action)
|
|
data = _build_rst(
|
|
username, password, cloud_audience_urn, endpoint_address, soap_action)
|
|
resp = http_client.post(endpoint_address, data=data, headers={
|
|
'Content-type':'application/soap+xml; charset=utf-8',
|
|
'SOAPAction': soap_action,
|
|
}, **kwargs)
|
|
if resp.status_code >= 400:
|
|
logger.debug("Unsuccessful WsTrust request receives: %s", resp.text)
|
|
# It turns out ADFS uses 5xx status code even with client-side incorrect password error
|
|
# resp.raise_for_status()
|
|
return parse_response(resp.text)
|
|
|
|
|
|
def escape_password(password):
|
|
return (password.replace('&', '&').replace('"', '"')
|
|
.replace("'", ''') # the only one not provided by cgi.escape(s, True)
|
|
.replace('<', '<').replace('>', '>'))
|
|
|
|
|
|
def wsu_time_format(datetime_obj):
|
|
# WsTrust (http://docs.oasis-open.org/ws-sx/ws-trust/v1.4/ws-trust.html)
|
|
# does not seem to define timestamp format, but we see YYYY-mm-ddTHH:MM:SSZ
|
|
# here (https://www.ibm.com/developerworks/websphere/library/techarticles/1003_chades/1003_chades.html)
|
|
# It avoids the uncertainty of the optional ".ssssss" in datetime.isoformat()
|
|
# https://docs.python.org/2/library/datetime.html#datetime.datetime.isoformat
|
|
return datetime_obj.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
|
|
|
|
def _build_rst(username, password, cloud_audience_urn, endpoint_address, soap_action):
|
|
now = datetime.utcnow()
|
|
return """<s:Envelope xmlns:s='{s}' xmlns:wsa='{wsa}' xmlns:wsu='{wsu}'>
|
|
<s:Header>
|
|
<wsa:Action s:mustUnderstand='1'>{soap_action}</wsa:Action>
|
|
<wsa:MessageID>urn:uuid:{message_id}</wsa:MessageID>
|
|
<wsa:ReplyTo>
|
|
<wsa:Address>http://www.w3.org/2005/08/addressing/anonymous</wsa:Address>
|
|
</wsa:ReplyTo>
|
|
<wsa:To s:mustUnderstand='1'>{endpoint_address}</wsa:To>
|
|
|
|
<wsse:Security s:mustUnderstand='1'
|
|
xmlns:wsse='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'>
|
|
<wsu:Timestamp wsu:Id='_0'>
|
|
<wsu:Created>{time_now}</wsu:Created>
|
|
<wsu:Expires>{time_expire}</wsu:Expires>
|
|
</wsu:Timestamp>
|
|
<wsse:UsernameToken wsu:Id='ADALUsernameToken'>
|
|
<wsse:Username>{username}</wsse:Username>
|
|
<wsse:Password>{password}</wsse:Password>
|
|
</wsse:UsernameToken>
|
|
</wsse:Security>
|
|
|
|
</s:Header>
|
|
<s:Body>
|
|
<wst:RequestSecurityToken xmlns:wst='{wst}'>
|
|
<wsp:AppliesTo xmlns:wsp='http://schemas.xmlsoap.org/ws/2004/09/policy'>
|
|
<wsa:EndpointReference>
|
|
<wsa:Address>{applies_to}</wsa:Address>
|
|
</wsa:EndpointReference>
|
|
</wsp:AppliesTo>
|
|
<wst:KeyType>{key_type}</wst:KeyType>
|
|
<wst:RequestType>{request_type}</wst:RequestType>
|
|
</wst:RequestSecurityToken>
|
|
</s:Body>
|
|
</s:Envelope>""".format(
|
|
s=Mex.NS["s"], wsu=Mex.NS["wsu"], wsa=Mex.NS["wsa10"],
|
|
soap_action=soap_action, message_id=str(uuid.uuid4()),
|
|
endpoint_address=endpoint_address,
|
|
time_now=wsu_time_format(now),
|
|
time_expire=wsu_time_format(now + timedelta(minutes=10)),
|
|
username=username, password=escape_password(password),
|
|
wst=Mex.NS["wst"] if soap_action == Mex.ACTION_13 else Mex.NS["wst2005"],
|
|
applies_to=cloud_audience_urn,
|
|
key_type='http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer'
|
|
if soap_action == Mex.ACTION_13 else
|
|
'http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey',
|
|
request_type='http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue'
|
|
if soap_action == Mex.ACTION_13 else
|
|
'http://schemas.xmlsoap.org/ws/2005/02/trust/Issue',
|
|
)
|
|
|