Open jenshnielsen opened 2 weeks ago
Thanks for the logs that's super helpful for telling what's going on.
So what changed in https://github.com/astral-sh/uv/pull/2976 is we use the URL as the service name for keyring before using just the host. Is it possible your keyring backend is returning different credentials in this case?
I don't see anything that could be going wrong on our end in those logs. In both cases, we find credentials in the keyring and use them for the request.
Thanks @zanieb I don't necessarily think this broke with #2976 but before that PR cannot easily test this since there was no way for me to inject the username. If useful I can manually recompile version 0.1.33 with the hardcoded username changed to match what Azure Artifacts expects.
I would not expect the keyring responsens to be different between the 2 requests but perhaps its possible.
You could manually call keyring <url> <user>
and see if it is idempotent.
I don't necessarily think this broke with https://github.com/astral-sh/uv/pull/2976 but before that PR cannot easily test this since there was no way for me to inject the username.
Ah thanks for clarifying. I wouldn't bother trying an older version in that case.
@zanieb Found the problem by adding the credentials to the trace message when the credentials are found. e.g something like
debug!("Found credentials in keyring for {url} as {credentials:?}");
The issue is that artifacts-keyring (the keyring extension that azure artifacts uses) may print information like:
[Information] [CredentialProvider]VstsCredentialProvider - Acquired bearer token using 'MSAL Silent'\r\r\n[Information] [CredentialProvider]VstsCredentialProvider - Attempting to exchange the bearer token for an Azure DevOps session token.\r\r\n
On the first fetch from keyring. This currently leaks into the credentials which naturally are not valid then.
Oh wow, like that goes to stdout and so we consider them "part of" the credentials?
@charliermarsh Yes that seems to be the case.
@zanieb - We may want to instead take the last line rather than the full output? \cc @jaraco in case you have advice on the contract that we should respect from the keyring CLI.
Err, wait, it looks like we might be inspecting both stdout
and stderr
? I'll fix that.
Nevermind, we're only collecting stdout
, I misread.
It does not look like the implementation in pip does anything special https://github.com/pypa/pip/blob/main/src/pip/_internal/network/auth.py#L145 It reads stdout and strips newline.
However, for some reason that I do not understand calling keyring from uv seems to result in a token exchange much more frequently than from pip making this much easier to observe.
Very strange that there's any difference in behavior here then.
@charliermarsh I am guessing the main reason that this goes relatively unnoticed with pip is that the keyring CLI is only a fallback for the in-process keyring implementation which does not have this issue.
For reference the issue is likely this code https://github.com/microsoft/artifacts-keyring/blob/master/src/artifacts_keyring/plugin.py#L116-L119 which converts stderr from the .net tool that artifacts-keyring wraps to stdout.
Nice investigation! Thank you!
I'm not sure we can just read the last line of output, there's nothing guaranteeing the password does not include new lines. We recently discussed this at https://github.com/jaraco/keyring/pull/678#discussion_r1567891980. Frankly this sounds like bad behavior by the Azure Artifacts keyring plugin — we should open an issue requesting that they change it to stream the logs to stderr as appropriate.
It seems really hard to guarantee that nothing will print to stdout
when you're loading arbitrary Python plugins and packages :/
Could it write its output to a file instead?
That's true but like.. we're not differing from pip here and this is the contract that keyring provides. We're adding a JSON output format to keyring which might sort of help... but yeah writing to an output file seems like the only way to avoid confusion. Even if we get that added to keyring though, only the people on the latest keyring version will benefit from it and it's expensive for us to check what keyring version is being used.
We should probably just provide an option to invoke a Python interpreter to call into keyring.
Yeah that's a bummer. I don't think JSON output helps either since we won't know where to delimit the JSON.
No great suggestions here.
Very strange that there's any difference in behavior here then.
So I think the reason there is a difference goes like the following.
To redeem the immediate problem, I have submitted https://github.com/microsoft/artifacts-keyring/pull/73 which does resolve the issue for me but of cause does nothing to fix the general problem.
@zanieb - We may want to instead take the last line rather than the full output? \cc @jaraco in case you have advice on the contract that we should respect from the keyring CLI.
The keyring CLI hasn't been designed around having a stable interface. Moreover, it cannot in general, as the backends are pluggable (the VSTS backend and the output it emits come from the plugin). The most reliable way to use keyring as an API would be to consume its API in Python (e.g. import keyring; keyring.get_credential(...)
), but of course, that would require to reach into a Python environment, which may not be as discoverable as an executable on the PATH.
Perhaps keyring
could offer a separate CLI API that would disable stdout and emit structured output. Or maybe it should do that by default if --format json
is indicated. Lots of options here, probably worthy of a design discussion, given there's no obvious bug to fix.
We should probably just provide an option to invoke a Python interpreter to call into keyring.
Sounds like you may be on the same page. Do be careful here, as the behavior of keyring behavior is highly dependent on the backends installed, so you'll need to be sure to have the same backends installed as the keyring
command has.
If it helps, the keyring
command could emit its PYTHONPATH
and sys.executable
for an API caller to call into (not sure I love that idea 😬 ). If only our operating systems had a framework for making RPC calls into installed applications.
One rather absurd but quite doable thing would be to allocate a pty and invoke keyring
with PYTHONINSPECT=1
and then send the python code in to use the keyring API internally.
Example with teetty:
PYTHONINSPECT=1 teetty -i ./stdin -- ./keyring
echo 'import keyring; open("/tmp/out.txt", "w").write(keyring.get_password("system", "username") or ""); quit()' > ./stdin
cat /tmp/out.txt
For example running
with the following env variables set
the first time fails but rerunning it again works correctly.
UV version 0.1.38 but has been an issue since support for this type of auth landed in #2976
Redacted log of non working execution
Redacted log of working execution