record-compat: check every credential before reading anything
A wrong tenant-administrator password failed only once the script had already enumerated every account, on a server that may not be up for long. All the identities are now checked against the session endpoint first, and a refusal names each one that failed, with the two things that usually explain it: basic authentication wants the account's name rather than its email address, and an account with two-factor or OAuth-only sign-in needs an app password. It also gives the curl line to test one on its own. The unreachable message no longer suggests --insecure for a refused connection; that hint is now only for a certificate it couldn't verify.
This commit is contained in:
@@ -78,8 +78,27 @@ class Client:
|
|||||||
fail(f'{self.name} did not authenticate: {detail}', code=1)
|
fail(f'{self.name} did not authenticate: {detail}', code=1)
|
||||||
fail(f'{self.name}: HTTP {error.code} from {url}: {detail}', code=1)
|
fail(f'{self.name}: HTTP {error.code} from {url}: {detail}', code=1)
|
||||||
except urllib.error.URLError as error:
|
except urllib.error.URLError as error:
|
||||||
fail(f'cannot reach {url}: {error.reason}. '
|
self._unreachable(error)
|
||||||
f'A self-signed certificate needs --insecure.', code=1)
|
|
||||||
|
def authenticates(self):
|
||||||
|
"""Whether these credentials are accepted, without failing the run."""
|
||||||
|
request = urllib.request.Request(
|
||||||
|
f'{self.server}/jmap/session',
|
||||||
|
headers={'Authorization': f'Basic {self.auth}'})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, context=self.ctx, timeout=30) as response:
|
||||||
|
return response.status == 200
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
if error.code in (401, 403):
|
||||||
|
return False
|
||||||
|
fail(f'{self.name}: HTTP {error.code} from the session endpoint', code=1)
|
||||||
|
except urllib.error.URLError as error:
|
||||||
|
self._unreachable(error)
|
||||||
|
|
||||||
|
def _unreachable(self, error):
|
||||||
|
hint = ('. A self-signed certificate needs --insecure'
|
||||||
|
if isinstance(error.reason, ssl.SSLError) else '')
|
||||||
|
fail(f'cannot reach {self.server}: {error.reason}{hint}', code=1)
|
||||||
|
|
||||||
def call(self, method, arguments):
|
def call(self, method, arguments):
|
||||||
"""One JMAP method call; returns its response object."""
|
"""One JMAP method call; returns its response object."""
|
||||||
@@ -204,14 +223,23 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
admin = Client(args.server, args.admin, args.insecure)
|
admin = Client(args.server, args.admin, args.insecure)
|
||||||
session = admin._post(f'{args.server.rstrip("/")}/jmap', {'using': USING, 'methodCalls': []})
|
|
||||||
if 'methodResponses' not in session:
|
|
||||||
fail(f'{admin.name} signed in but the server returned no JMAP session', code=1)
|
|
||||||
|
|
||||||
tenant_admins = [(Client(args.server, credentials, args.insecure),
|
tenant_admins = [(Client(args.server, credentials, args.insecure),
|
||||||
credentials.split(':', 1)[1])
|
credentials.split(':', 1)[1])
|
||||||
for credentials in args.tenant_admin]
|
for credentials in args.tenant_admin]
|
||||||
|
|
||||||
|
# Every identity first, before any work: a recording that stops on the
|
||||||
|
# last tenant administrator has wasted a pass over every account, and
|
||||||
|
# this runs against a server that may not be up for long.
|
||||||
|
refused = [client.name for client in [admin] + [c for c, _ in tenant_admins]
|
||||||
|
if not client.authenticates()]
|
||||||
|
if refused:
|
||||||
|
fail('these did not authenticate: ' + ', '.join(refused) + '.\n'
|
||||||
|
'Basic authentication wants the account\'s name, which may not be\n'
|
||||||
|
'its email address, and an account with two-factor or OAuth-only\n'
|
||||||
|
'sign-in needs an app password instead of its own. Check one with:\n'
|
||||||
|
" curl -s -o /dev/null -w '%{http_code}\\n' -u 'NAME:PASSWORD' "
|
||||||
|
f'{args.server.rstrip("/")}/jmap/session', code=1)
|
||||||
|
|
||||||
os.makedirs(args.out, exist_ok=True)
|
os.makedirs(args.out, exist_ok=True)
|
||||||
account_ids = admin.query_ids('Account')
|
account_ids = admin.query_ids('Account')
|
||||||
if not account_ids:
|
if not account_ids:
|
||||||
|
|||||||
Reference in New Issue
Block a user