Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable additional authentication methods to allow publishing to private Python repos #741

Open
golgor opened this issue Feb 22, 2024 · 20 comments

Comments

@golgor
Copy link

golgor commented Feb 22, 2024

Background

I'm working almost daily with Python at work and first of all I have to tell you that Rye is a blessing. It makes my work so much quicker and easier by automating and isolating the environments, it is almost crazy.

The company have an internal Python repository where we keep Python packages that we use internally. Currently I have it set up in a Github Action using Twine, but I would like to migrate to using Rye here as well as then it would be one complete solution.

From what I understand, Pypi is primarily trusting a token to enable authenatication but it seems Google is using a completely different method. I was never able to get the publishing working. The documentation does not cover anything about any other methods either.

The whole process in Github Action is generally:

  1. You use one of several methods together with the action google-github-actions/auth@v2
  2. You use gcloud cli to output authentication settings either using .pypirc or pip.conf

From what I understand, the recommended way is to use .pypirc.

Typical entry in .pypirc

[distutils]
index-servers =
    <Python repo name>

[toolsense-py-utils]
repository: <Python repo URL>
username: _json_key_base64
password:<KEY>

Typical entry in pip.conf

[global]
extra-index-url = https://_json_key_base64:<KEY>@<REPO URL>

Twine then uses these settings in either pip.conf or .pypirc when it uploads the files and the commands looks like:

python -m twine upload --repository <REPO_NAME> <FILES TO UPLOAD>

The issue

I tried to basically switch out twine upload with rye publish but as far as I can remember I just got an error that it was missing a token. As discussed above, gcloud doesn't use a token and as far as I know there are no way of generating one.

I tried to add the extra-index-url as --repository-url (from the pip.conf) in the rye publish as well (both with and without --repository option), but this didn't seem to do any difference.

I tried to look at the src, but I couldn't really figure out how things work. It does however seem that Rye primarily uses the token and doesn't consider the pip.conf or .pypirc.

It would be great if the Rye publish could match the capabilities of Twine. That would really make it a "The tool to rule them rule all" :)

I have the intention to learn Rust and would love to contribute to this project, but I'm too much of a beginner to solver something like this.

References

@mitsuhiko
Copy link
Collaborator

Rye just dispatches to twine. Is it not possible to pass --username and --password alongside the repo URL on the command line? Rye will persist it in a credentials file for future usage.

@golgor
Copy link
Author

golgor commented Feb 22, 2024

Rye just dispatches to twine. Is it not possible to pass --username and --password alongside the repo URL on the command line? Rye will persist it in a credentials file for future usage.

There are no real username and password for the authentication, but everything is stored in the .pypirc-file. If you just dispatch the call to Twine, it should work right away if the requirement for token is dropped?

@mitsuhiko
Copy link
Collaborator

I understand that you store this in .pypirc today but there is still username and password. Can you not pass them on the command line rather than pick them up from there?

@golgor
Copy link
Author

golgor commented Feb 22, 2024

Aha ok, so rye publish is basically an "alias" for Twine? I thought it only supported the options that were mentioned in the documentation. I can give it a try and see what happens.

@mitsuhiko
Copy link
Collaborator

It takes it's own options. Sorry the flags today are --username and --token where --token just means password.

@golgor
Copy link
Author

golgor commented Feb 22, 2024

@mitsuhiko
When I try to use rye publish --username <USERNAME> --repository-url <URL> --token <PASSWORD> I get the following error: error: invalid pypi url https://europe-west1-python.pkg.dev/<companz>/<company>-py-utils/ (use -h for help)

I also tested using only the --repository flag but then it automatically assumed pypi as well.

100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.1/10.1 kB • 00:00 • ?
WARNING  Error during upload. Retry with the --verbose option for more details.                                                     
ERROR    HTTPError: 403 Forbidden from https://upload.pypi.org/legacy/                                                              
         Username/Password authentication is no longer supported. Migrate to API Tokens or Trusted Publishers instead. See          
         https://pypi.org/help/#apitoken and https://pypi.org/help/#trusted-publishers

It seems to only accept publishing to pypi?
I will look around in the code and see if I can find anything obvious.

Edit:
I think I found the issue, is seems the code is made in such as way that it always require certain arguments. See here.

When I'm using twine to publish I only supply the command "upload" together with the --repository-argument and the files. This is not possible as it is hard-coded to always supply command such as username, password and url.

I got the debugger up and running, I will play around a little bit and maybe put together a PR.

Edit2:
FYI #743

@ischaojie
Copy link
Contributor

I think you should provide the --repository and --repository-url params together: rye publish --username <USERNAME> --repository <CUSTOM-NAME> --repository-url <URL> --token <PASSWORD>

@golgor
Copy link
Author

golgor commented Feb 23, 2024

I think you should provide the --repository and --repository-url params together: rye publish --username <USERNAME> --repository <CUSTOM-NAME> --repository-url <URL> --token <PASSWORD>

If I try this I get the error HTTPError: 403 Forbidden from <URL>. This is nothing that exists in the codebase for Rye so it is the response from Twine. Google Cloud have been trying to remove user-provided password due to security issues, so I guess this is the case here. If I use the exact same credentials using Twine and only providing the repository name, i.e. Twine fetches everything from my .pypirc, then everything works.

@cnpryer
Copy link
Contributor

cnpryer commented Feb 23, 2024

FWIW

rye-publish on  main [?] is 📦 v0.3.0 via 🐍 v3.12.1 
❯ rye publish --repository testpypi --repository-url https://test.pypi.org/legacy/ --username cnpryer --token <TOKEN> -y
Uploading distributions to https://test.pypi.org/legacy/
Uploading rye_publish-0.3.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.0/4.0 kB • 00:00 • ?
WARNING  Error during upload. Retry with the --verbose option for more details.                                                
ERROR    HTTPError: 403 Forbidden from https://test.pypi.org/legacy/                                                           
         Username/Password authentication is no longer supported. Migrate to API Tokens or Trusted Publishers instead. See     
         https://test.pypi.org/help/#apitoken and https://test.pypi.org/help/#trusted-publishers                               
error: failed to publish files

If you don't use --username __token__ will be used. So drop that. Then the problem is that we're writing these to the ~/.rye/credentials file. Could you remove those entries and try again?

❯ rye publish --repository testpypi --repository-url https://test.pypi.org/legacy/ --token <TOKEN> -y
Uploading distributions to https://test.pypi.org/legacy/
Uploading rye_publish-0.3.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.0/4.0 kB • 00:00 • ?
Uploading rye_publish-0.3.0.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.4/3.4 kB • 00:00 • ?

View at:
https://test.pypi.org/project/rye-publish/0.3.0/

@cnpryer
Copy link
Contributor

cnpryer commented Feb 23, 2024

I'd think adding .pypirc and OIDC support would be cool. If there's interest in this I could try to PR something this weekend, unless someone else gets to it before me. I can also try to scoop up some housecleaning for the publish code.

@cnpryer
Copy link
Contributor

cnpryer commented Feb 23, 2024

Note Twine will default to use API tokens.

IIUC it should pick up the username for non-PyPI usage, but I'm also curious of how your repository is set up.

@golgor
Copy link
Author

golgor commented Feb 23, 2024

@cnpryer that would be great. I started looking into it but with my experience in Rust this will take weeks.

The repository is set up according to the guildelines for setting up a Python-repo in Google Cloud.

To be honest, I have no idea exactly what is wrong. If I try to feed in all the information from command-line, I get an HTTP 403 (I.e. forbidden). So the connection works, but it rejects the authenatication for some reason. If I use twine with only the --repository-flag, it grabs everything from .pypirc and it works.

Might be something internally how twine sends the data depending on from where the input comes?

I'm pretty sure it is free to set up a Google Cloud account and a repo, might be something to consider as otherwise testing will be quite the hassle. I will of course be available for any testing or discussions.

@cnpryer
Copy link
Contributor

cnpryer commented Feb 23, 2024

Can you try and isolate the auth resource you're using? I believe Twine will use keyring creds if a password isn't provided, but, for me at least, it's hard to narrow down your exact issue without understanding your exact setup better.

If you're using a .pypirc file with a username and password you should be able to provide that with the usage described above.

If your pypirc doesn't include this then you might be relying on retrieving the credentials from your keyring setup, since IIRC Twine will fallback to it.

Of course if a MWE is shared I can check that out.

@golgor
Copy link
Author

golgor commented Feb 23, 2024

@cnpryer I'm not sure if I follow you.

I do authenticate with the .pypirc, it looks in my first post in this thread. I tried to change the password with one character in this file (Generated key of 256 bits or something) and I get a HTTP 401 error, i.e. Unauthorized. Changing the character back, everything works immediately.

So I can in other words confirm that it does read the content in the .pypirc-file. As mentioned, I tried to take all the information from the .pypirc-file and input it as arguments, see below:

python -m twine upload --username _json_key_base64 --repository toolsense-py-utils --repository-url https://europe-west1-python.pkg.dev/toolsense/toolsense-py-utils/ --password <hidden> --verbose dist/*
Uploading distributions to https://europe-west1-python.pkg.dev/toolsense/toolsense-py-utils/
INFO     dist/toolsense_erpnext-0.0.1b4-py3-none-any.whl (3.2 KB)                                                                                                                                                                                                                          
INFO     dist/toolsense_erpnext-0.0.1b4.tar.gz (7.6 KB)                                                                                                                                                                                                                                    
INFO     username set by command options                                                                                                                                                                                                                                                   
INFO     password set by command options                                                                                                                                                                                                                                                   
INFO     username: _json_key_base64                                                                                                                                                                                                                                                        
INFO     password: <hidden>                                                                                                                                                                                                                                                                
Uploading toolsense_erpnext-0.0.1b4-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.1/10.1 kB • 00:00 • ?
INFO     Response from https://europe-west1-python.pkg.dev/toolsense/toolsense-py-utils/:                                                                                                                                                                                                  
         403 Forbidden                                                                                                                                                                                                                                                                     
INFO     Permission "artifactregistry.repositories.uploadArtifacts" denied on resource "projects/toolsense/locations/europe-west1/repositories/toolsense-py-utils" (or it may not exist)                                                                                                   
                                                                                                                                                                                                                                                                                           
ERROR    HTTPError: 403 Forbidden from https://europe-west1-python.pkg.dev/toolsense/toolsense-py-utils/                                                                                                                                                                                   
         Forbidden

Looking in the .pypirc-file I assumed the following:

Running the command without --repository-url, --username and --password works fine:

python -m twine upload --repository toolsense-py-utils --verbose dist/* 
INFO     Using configuration from /home/golgor/.pypirc                                                                                                                                                                                                                                     
Uploading distributions to https://europe-west1-python.pkg.dev/toolsense/toolsense-py-utils/
INFO     dist/toolsense_erpnext-0.0.1b4-py3-none-any.whl (3.2 KB)                                                                                                                                                                                                                          
INFO     dist/toolsense_erpnext-0.0.1b4.tar.gz (7.6 KB)                                                                                                                                                                                                                                    
INFO     username set from config file                                                                                                                                                                                                                                                     
INFO     password set from config file                                                                                                                                                                                                                                                     
INFO     username: _json_key_base64                                                                                                                                                                                                                                                        
INFO     password: <hidden>                                                                                                                                                                                                                                                                
Uploading toolsense_erpnext-0.0.1b4-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.1/10.1 kB • 00:00 • ?
INFO     Response from https://europe-west1-python.pkg.dev/toolsense/toolsense-py-utils/:                                                                                                                                                                                                  
         200 OK                                                                                                                                                                                                                                                                            
Uploading toolsense_erpnext-0.0.1b4.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.6/14.6 kB • 00:00 • ?
INFO     Response from https://europe-west1-python.pkg.dev/toolsense/toolsense-py-utils/:                                                                                                                                                                                                  
         200 OK  

These commands where executed within a few minutes in between in the same order as they are written down. I do note one difference in username set by command options vs username set from config file. You can also see in the latter command that is is "Using configurations from /home/golgor/.pypirc. Which is the one file I have been looking at and copied the information from.

Apart from that I can't really spot any differences. This is obviously something with Twine, but it is a fact that it works if you follow the guildlines from Google, i.e. only supply the repository and get the rest of the information from the .pypirc instead of supplying it via the CLI. And there is the problem with the current implementation in Rye, as it is hardcode to always supply certain arguments, and bail if they are not correct or missing.

So either we dig into this deeper and figure out why Twine behaves as it does, or we just accept how it is and refactor the code to be more flexible and only pass the arguments that is actually supplied.

@cnpryer
Copy link
Contributor

cnpryer commented Feb 24, 2024

I read through https://cloud.google.com/artifact-registry/docs/python/authentication#search-order a little. It's hard to learn more about your problem without having experience with the platform or having time to learn about it enough to test your issue.

Are you using the Artifact Registry keyring backend?

That key you're using looks like it's an encoded key.

@cnpryer
Copy link
Contributor

cnpryer commented Feb 24, 2024

If we want something quick for now, cnpryer:0a76694222adec09ea450c5fc5fd041d1518fdf7 will hopefully have:

  • Refactored the publish logic to allow for Twine dispatch without passwords
  • New --skip-save-credentials flag
  • TODO: --yes -> --non-interactive - Need to decide how Twine without password handles "keyring or prompt". If we use --yes do we just provide --non-interactive for now?
  • TODO: I'd like tests for at least configuration resolution. Would be nice to have mock of pypi and non-pypi.

I'm also unsure if there are plans to either continue leaning on Twine the way we are, or if there are plans to implement more of the publish process ourselves.

Branch: main...cnpryer:rye:cp-publish-twine

@golgor
Copy link
Author

golgor commented Feb 24, 2024

@cnpryer
Yes, I'm using the keyring approach, more specifically Keyring authentication with service account credentials. The thing is that everything is handled by Twine and you don't necessarily have to learn about how everything works and is set up.

The only thing required is to provide a more direct interface to the internal Twine. I only need to use the --repository-argument, and nothing else. With the current setup this is not possible as several other arguments are hard-coded.

Having for example an interface that just passes the arguments to Twine without to much checking and modification of data and everything will work. I have a PR, I can make a simple example in a PR to make it more clear.

For example using a similar solution to below for all arguments would solve my issue:

if let Some(cert) = cmd.cert {
    publish_cmd.arg("--cert").arg(cert);
}

These solutions is what creates my problem:

let repository_url = match cmd.repository_url {
    Some(url) => url,
    None => {
        let default_repository_url = Url::parse("https://upload.pypi.org/legacy/")?;
        credentials
            .get(repository)
            .and_then(|table| table.get("repository-url"))
            .map(|url| match Url::parse(&escape_string(url.to_string())) {
                Ok(url) => url,
                Err(_) => default_repository_url.clone(),
            })
            .unwrap_or(default_repository_url)
    }
};

The reason being, that if I don't provide a URL, there is logic to assume pypi. This is however not always true, as my case proves. There are other sources of this information than the command line arguments.

That might even be the whole reason why the code looks like it does. It assumes all necessary information must come from the CLI.

I will have a look and give your branch a test. Thanks!

@golgor
Copy link
Author

golgor commented Feb 24, 2024

If we want something quick for now, this branch will hopefully have:

  • Refactored the publish logic to allow for Twine dispatch without passwords
  • New --skip-save-credentials flag
  • TODO: --yes -> --non-interactive - Need to decide how Twine without password handles "keyring or prompt". If we use --yes do we just provide --non-interactive for now?
  • TODO: I'd like tests for at least configuration resolution. Would be nice to have mock of pypi and non-pypi.

I'm also unsure if there are plans to either continue leaning on Twine the way we are, or if there are plans to implement more of the publish process ourselves.

I was actually thinking of setting one up. I have a personal Google Cloud account primarily for playing around. I can see if I can set something up.

@cnpryer
Copy link
Contributor

cnpryer commented Feb 24, 2024

If I have time later I'll finish up a first-pass. I can use test.pypi.org to manually test the changes. I wouldn't do anything with that branch yet. It's definitely not done.

I'm sharing the branch here in case this comment becomes relevant:

I'm also unsure if there are plans to either continue leaning on Twine the way we are, or if there are plans to implement more of the publish process ourselves.

Or if we'd like a quick fix like this and there's some feedback on the solution I'm working on.

@ohmycloud
Copy link

rye publish `
  --repository devpi `
  --repository-url http://pypi.private.repository `
  --username test`
  --token a_very_complex_password `
  --skip-existing

Providing both --repository and --repository-url is fine for me, and use --token instead of --password. It's too long and a little tedious to type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants