conan-auth-source-plugin 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,45 @@
1
+ """
2
+ The implementation of the auth source plugin
3
+ """
4
+ import logging
5
+ from typing import Optional
6
+
7
+ from auth_source.authenticator import AuthenticatorFactory
8
+ from auth_source.config import make_config
9
+ from auth_source.source_credentials import load_source_credentials, find_first_credentials
10
+ from auth_source.types import AuthSourceDetails
11
+
12
+ # A conan authentication plugin is not given the conan api or a logger, as it
13
+ # might cause recursion problems. Since the logger is not available, reach out
14
+ # and get the logger named conan.
15
+ logger = logging.getLogger("conan")
16
+
17
+
18
+ def plugin(url: str) -> Optional[AuthSourceDetails]:
19
+ """
20
+ A Conan authentication for source credentials authenticator.
21
+
22
+ If the source URL is configured to support generating a github application token
23
+ then return a valid token.
24
+ """
25
+ logging.debug("Loading 'source_credentials.json'")
26
+ source_credentials = load_source_credentials()
27
+ if source_credentials:
28
+ logging.info("Finding credentials for '%s'", url)
29
+ credentials = find_first_credentials(url, source_credentials)
30
+ if credentials:
31
+ config = make_config(credentials)
32
+ if config:
33
+ auth = AuthenticatorFactory.make_authenticator(url, config.type)
34
+ token = auth.make_token(config)
35
+ if token:
36
+ headers = credentials.get("headers")
37
+ return {"token": token, "headers": headers} if headers else {"token": token}
38
+ logger.warning("Failed to get github application token '%s'", url)
39
+ else:
40
+ logger.warning("Failed to make configuration for '%s'", url)
41
+ else:
42
+ logger.info("No 'source_credentials.json' configuration for '%s'", url)
43
+ else:
44
+ logger.warning("No 'source_credentials.json' found")
45
+ return None
@@ -0,0 +1,52 @@
1
+ """
2
+ Plugable authenticator support
3
+ """
4
+ import logging
5
+ from typing import Protocol, runtime_checkable, Optional
6
+
7
+ from github import GithubIntegration
8
+
9
+ from auth_source.config import Config
10
+
11
+ logger = logging.getLogger("conan")
12
+
13
+
14
+ @runtime_checkable
15
+ class Authenticator(Protocol):
16
+ """
17
+ Interface for authenticator making an authentication token
18
+ """
19
+
20
+ def make_token(self, config: Config) -> Optional[str]:
21
+ """
22
+ Make a token given a configuration.
23
+
24
+ The token is a short-lived (1 hour) cryptographic string starting with 'ghs_'.
25
+ """
26
+ ...
27
+
28
+
29
+ class GithubApplicationAuthenticator(Authenticator):
30
+
31
+ def make_token(self, config: Config) -> str:
32
+ """
33
+ Create an authentication token for the given github application id.
34
+ """
35
+ logger.info("GitHub App %d auth for Installation ID: %d", config.id, config.installation_id)
36
+
37
+ integration = GithubIntegration(config.id, config.private_key)
38
+ auth_connection = integration.get_access_token(config.installation_id)
39
+ return auth_connection.token
40
+
41
+
42
+ class AuthenticatorFactory:
43
+ """
44
+ A factory for making authentication instances. Only one so far...
45
+ """
46
+
47
+ @staticmethod
48
+ def make_authenticator(_url: str, _auth_type: Optional[str]) -> Authenticator:
49
+ """
50
+ For now, only a github application authenticator is supported.
51
+ """
52
+ return GithubApplicationAuthenticator()
auth_source/config.py ADDED
@@ -0,0 +1,81 @@
1
+ """
2
+ Config POCO
3
+ """
4
+ import os
5
+ from dataclasses import dataclass
6
+ from typing import Optional, Any
7
+
8
+ from auth_source.types import CredentialBlock
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Config:
13
+ """
14
+ A top level state to hold command line arguments and program state.
15
+ """
16
+ type: Optional[str]
17
+ """
18
+ What type of authentication should be performed to get the token.
19
+
20
+ This is expected to be set if multiple authentication implementations are support. At
21
+ this stage this is a github application only provider, so the value is ignored (for now).
22
+ What we can see is that bitbucket self hosted doesn't support this style of auth (and a static
23
+ token is as good as it gets).
24
+ """
25
+
26
+ id: str | int
27
+ """
28
+ The application id used for authentication.
29
+
30
+ Identifies your application globally to sign JSON Web Tokens (JWTs). This id is
31
+ generated by github when the application is created.
32
+
33
+ Note: This **not** the app name, the client id, or the app slug.
34
+
35
+ """
36
+
37
+ private_key: str
38
+ """
39
+ A string containing the installation access
40
+ """
41
+
42
+ installation_id: int
43
+ """
44
+ The installation id used for authentication.
45
+
46
+ Identifies a specific account (user/org) that installed your app. This is an integer that
47
+ is createdby github when a user installs the application.
48
+ """
49
+
50
+
51
+ def make_config(credential_block: Optional[CredentialBlock]) -> Optional[Config]:
52
+ """
53
+ Create an authentication configuration for the given github application if possible.
54
+ """
55
+
56
+ # If the credential block has a credential, then don't attempt to authenticate. It is assumed
57
+ # that a statically define token or user/password means we don't need to get an authentication token.
58
+ # Note: the main Conan logic requires a token or a user/password or headers. This might create
59
+ # an issue in the future and highlights how this plugin is using the `source_credentials.json`
60
+ # in an adhoc unapproved way.
61
+ if not credential_block.get("token") and not credential_block.get("user"):
62
+
63
+ app_id = _get_value_with_fallbacks(credential_block, "app_id", "CONAN_GITHUB_APP_ID")
64
+ private_key = _get_value_with_fallbacks(credential_block, "app_private_key", "CONAN_GITHUB_APP_PRIVATE_KEY")
65
+ installation_id = _get_value_with_fallbacks(credential_block, "app_installation_id",
66
+ "CONAN_GITHUB_APP_INSTALLATION_ID")
67
+ if all([app_id, private_key, installation_id]):
68
+ return Config(
69
+ type=credential_block.get("type"),
70
+ id=app_id,
71
+ private_key=private_key,
72
+ installation_id=installation_id)
73
+ return None
74
+
75
+
76
+ def _get_value_with_fallbacks(data: Optional[CredentialBlock], data_name: str, primary_env: str) -> Optional[Any]:
77
+ """
78
+ Checks a dictionary key, then a primary environment variable,
79
+ then a backup environment variable, defaulting to None.
80
+ """
81
+ return data.get(data_name) or os.getenv(primary_env) or None
@@ -0,0 +1,55 @@
1
+ """
2
+ A script to install the cci-build command into Conan.
3
+ """
4
+ import logging
5
+ import os
6
+ import shutil
7
+ from importlib.resources import files, as_file
8
+ from pathlib import Path
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ def get_conan_home() -> Path:
14
+ """
15
+ Resolve Conan 2 home directory.
16
+
17
+ This is the root directory where Conan stores:
18
+ - cache
19
+ - extensions
20
+ - configuration
21
+ - remotes
22
+
23
+ Resolution order:
24
+ 1. CONAN_HOME environment variable (if set)
25
+ 2. Default platform-specific location (~/.conan2)
26
+
27
+ Returns:
28
+ Path to Conan home directory.
29
+ """
30
+
31
+ # 1. Explicit override (highest priority)
32
+ env_home = os.environ.get("CONAN_HOME")
33
+ if env_home:
34
+ return Path(env_home).expanduser().resolve()
35
+
36
+ # 2. Default Conan 2 home directory
37
+ # Conan 2 standard default is ~/.conan2 unless overridden
38
+ return Path.home() / ".conan2"
39
+
40
+
41
+ def main():
42
+ """
43
+ Entry point for the conan auth source installer. This entry point is
44
+ installed by the setup.py script when the module is installed.
45
+ """
46
+ conan_home = get_conan_home()
47
+ target_dir = conan_home / "extensions" / "plugins"
48
+ target_dir.mkdir(parents=True, exist_ok=True)
49
+ destination = target_dir / "auth_source.py"
50
+
51
+ resource = files("extensions.plugins").joinpath("auth_source.py")
52
+ with as_file(resource) as src:
53
+ shutil.copyfile(src, destination)
54
+
55
+ log.info("Installed Conan auth source plugin to '%s'", destination)
@@ -0,0 +1,42 @@
1
+ import json
2
+ import os
3
+ import platform
4
+ from typing import Optional
5
+
6
+ from conan.errors import ConanException
7
+ from conan.internal.paths import get_conan_user_home
8
+ from conan.internal.util.files import load
9
+ from jinja2 import Template
10
+
11
+ from auth_source.types import SourceCredentials, CredentialBlock
12
+
13
+
14
+ def load_source_credentials() -> Optional[SourceCredentials]:
15
+ """
16
+ Reach out into Conan and load the source credentials file. This is not provided as a
17
+ plugin API parameter, so this needs to be loaded again. The `source_credentials.json`
18
+ file is a jinja2 template that must render as JSON.
19
+ """
20
+ home_folder = get_conan_user_home()
21
+ creds_path = os.path.join(home_folder, "source_credentials.json")
22
+ if os.path.exists(creds_path):
23
+ try:
24
+ template = Template(load(creds_path))
25
+ content = template.render({"platform": platform, "os": os})
26
+ return json.loads(content)
27
+ except Exception as e:
28
+ raise ConanException(f"Error loading 'source_credentials.json' {creds_path}: {repr(e)}")
29
+ return None
30
+
31
+
32
+ def find_first_credentials(url: str, source_credentials: Optional[SourceCredentials]) -> Optional[CredentialBlock]:
33
+ """
34
+ Find the first credential block that has a 'url' that is at the start of the source url.
35
+ """
36
+ credentials_list = source_credentials.get("credentials", [])
37
+ for block in credentials_list:
38
+ # Extract the configuration URL pattern from the current block
39
+ config_url = block.get("url")
40
+ if config_url and url.startswith(config_url):
41
+ return block
42
+ return None # not found
auth_source/types.py ADDED
@@ -0,0 +1,38 @@
1
+ """
2
+ Python typing definitions.
3
+ """
4
+ from typing import Dict, Optional, TypedDict, List
5
+
6
+ CredentialBlock = TypedDict(
7
+ "CredentialBlock",
8
+ {
9
+ "url": str,
10
+ "username": Optional[str],
11
+ "password": Optional[str],
12
+ "token": Optional[str],
13
+ "headers": Optional[Dict[str, str]],
14
+ "type": str,
15
+
16
+ "app_id": str,
17
+ "app_installation_id": Optional[str],
18
+ "app_private_key": Optional[str],
19
+ },
20
+ )
21
+
22
+ # 2. Define the outer file layout without using a class
23
+ SourceCredentials = TypedDict(
24
+ "SourceCredentials",
25
+ {
26
+ "credentials": List[CredentialBlock]
27
+ }
28
+ )
29
+
30
+ AuthSourceDetails = TypedDict(
31
+ "AuthSourceDetails",
32
+ {
33
+ "token": Optional[str],
34
+ "username": Optional[str],
35
+ "password": Optional[str],
36
+ "headers": Optional[Dict[str, str]],
37
+ }
38
+ )
@@ -0,0 +1,28 @@
1
+ """
2
+ A Conan plugin module.
3
+ """
4
+
5
+
6
+ def auth_source_plugin(url: str, **kwargs):
7
+ """
8
+ This is a conan plugin, that conforms to the plugin definition in Conan.
9
+
10
+ This implementation thunks the call through to the plugin entry point
11
+ in the Conan auth source authentication plugin module.
12
+
13
+ In the current implementation, defined here:
14
+ - https://github.com/conan-io/conan/blob/develop2/conan/internal/rest/conan_requester.py#L66
15
+
16
+ At the time of writing, Conan invokes this plugin with the request URL and
17
+ no additional keyword arguments. Perhaps in the future Conan could pass:
18
+ - a logger instance
19
+ - the best matching section of the `source_credentials.json` file, or the whole contents
20
+ - the `www-authenticate` headers upon a 401 request failing
21
+ """
22
+
23
+ # Import the python module at this point, so that module loading issues
24
+ # are reported during authentication.
25
+ #
26
+ # pylint: disable=import-outside-toplevel
27
+ from auth_source.auth_source_plugin import plugin
28
+ return plugin(url, **kwargs)
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: conan-auth-source-plugin
3
+ Version: 0.0.5
4
+ Summary: A python module to provide GitHub application authorisation for source downloads
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: conan<3.0,>=2.25
9
+ Requires-Dist: packaging>=26.2
10
+ Requires-Dist: PyGithub>=2.9.1
11
+ Dynamic: description
12
+ Dynamic: description-content-type
13
+ Dynamic: license-file
14
+ Dynamic: requires-dist
15
+ Dynamic: requires-python
16
+ Dynamic: summary
17
+
18
+ # [conan-auth-source-plugin](https://github.com/conan-py/conan-auth-source-plugin)
19
+
20
+ A Conan authentication source plugin, implemented as a python module. Conan supports both a *remote*
21
+ and a *source* authentication, where the remotes are used for interacting with a remote for
22
+ packages, whereas the *source* is used for getting source using the `get()` or `download()`
23
+ method in a `conanfile.py`.
24
+
25
+ # Installation
26
+
27
+ Install the conan authentication source Python module, then install the plugin in that module
28
+ into the conan installation.
29
+
30
+ ```shell
31
+ python -m pip install conan-auth-source-plugin
32
+ conan-auth-source-plugin-install
33
+ ```
34
+
35
+ # Configuration
36
+
37
+ This plugin uses the
38
+ [`source_creditials.json`](https://docs.conan.io/2/reference/config_files/source_credentials.html)
39
+ file for configuration. This file is marked as experimental at this stage. This module further
40
+ experiments and extends its usage.
41
+
42
+ **Note**: The conan source configuration code uses a first match (begins with) strategy with URLs.
43
+ Thus, it is important to order the credentials in longest url first if there is any overlap in
44
+ matching the URL being fetched with the configuration.
45
+
46
+ ## Sample
47
+
48
+ ```json
49
+ {
50
+ "credentials": [
51
+ {
52
+ "url": "https://github.com/...",
53
+ "type": "github.app",
54
+ "app_id": "4459267",
55
+ "app_installation_id": "150620648",
56
+ "app_private_key" : "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
57
+ }
58
+ ]
59
+ }
60
+ ```
61
+
62
+ # Create App
63
+
64
+ The following procedure can be used to create an application in github. This procedure requires an
65
+ organisation owner, or a team member with app management permissions.
66
+
67
+ This procedure is documented as a GitHub app that act on their
68
+ [own behalf](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps#github-apps-that-act-on-their-own-behalf).
69
+ This requires an installation access token for authentication.
70
+
71
+ 1. Go to the organisation (or user) settings
72
+
73
+ https://github.com/organizations/conan-py/settings/apps
74
+ https://github.com/<org name>/conan-py/settings/apps
75
+
76
+ 2. App settings
77
+
78
+ - provide a github app name
79
+ - write a description
80
+ - add a homepage URL (e.g. to the organisation landing page), even though it isn't explicitly used
81
+ - disable web hook
82
+ - add repository permission 'Contents', set to 'Read-Only'
83
+
84
+ 3. Once the app is created, the 'App ID' and the 'Client ID' (not used for this workflow) are known
85
+
86
+ 4. Go to the bottom of the application and generate a private key. This will
87
+ generate a 2048bit RSA key pair without a pass phrase.
88
+
89
+ 5. On the side bar of the application, select the "Install App" menu item. Once installed
90
+ the installation id can be taken from the installation URL. For example if the installation
91
+ URL is 'https://github.com/organizations/conan-py/settings/installations/150620648', then the
92
+ installation id is 150620648. The installation id is not displayed in the web UI of github.
93
+
94
+ # Why use this plugin module
95
+
96
+ This module is a shift-left style strategy for authentication. Instead of using this pluing
97
+ a build pipeline (or any Conan build) can pre-authenticate with all github organisation/repositores
98
+ that *may* be needed during a build.
99
+
100
+ This module goes half-way towards "authentication on demand". This is a concept where http
101
+ authentication is only attempted by a client if a
102
+ [401 (Not authenticated)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/401)
103
+ response is received, and the
104
+ [`WWW-Authenticate` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/WWW-Authenticate)
105
+ provides
106
+ [authentication schemes](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml)
107
+ that are semantically understood by the client. Once authenticated the client should reissue the
108
+ http request with the new credentials.
109
+
110
+ *Residual*: This plugin does not defer authentication until after it is needed. This is not
111
+ supported by the Conan client.
112
+
113
+ # Known issues
114
+
115
+ - this implementation stores private keys by value in memory. e.g. if using AWS it
116
+ would be better to use a HSM or AWS KMS, but this would require an implementation
117
+ of the `github.Auth.Auth` class. Using AWS Secrets Manager at least stores the
118
+ key more securely at rest, even though it will be stored in memory non-securely.
119
+
120
+ - when using the `get()` method in a `conanfile.py`, provide a filename parameter
121
+ with a representative name (e.g. 'archive.tgz') so that conan can write the download
122
+ to disk. The filename should be expressed in the `conandat.yml`.
123
+
124
+ # Development
125
+
126
+ To install the plugin in a local Conan environment, the whole of the plugin repository
127
+ can be installed, as the `.conanignore` will exclude everything except the plugin
128
+ python file that thunks to the module.
129
+
130
+ ```shell
131
+ conan config install .
132
+ ```
133
+
134
+ Install the plugin Python for development as an editable module. From the root of the
135
+ repository/project:
136
+
137
+ ```shell
138
+ pip install --editable .
139
+ ```
140
+
141
+ # Links
142
+
143
+ - https://pypi.org/project/conan-auth-source-plugin
144
+ - https://github.com/conan-io/conan-extensions/tree/main
145
+ - https://docs.conan.io/2/reference/extensions/authorization_plugins.html
146
+ - https://docs.conan.io/2/reference/config_files/source_credentials.html
147
+ - https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app
148
+ - https://docs.github.com/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps
149
+
150
+ ## pygithub
151
+
152
+ - https://github.com/PyGithub/PyGithub/tree/main
153
+ - https://pygithub.readthedocs.io/en/stable/introduction.html
154
+
155
+ # Appendices
156
+
157
+ ## github tarball URLs
158
+
159
+ For getting source from a private github repo, use a URL/http request of the form:
160
+
161
+ ```
162
+ GET https://api.github.com/repos/{owner}/{repo}/tarball/{ref}
163
+ Authorization: Bearer <installation_token>
164
+ ```
@@ -0,0 +1,14 @@
1
+ auth_source/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ auth_source/auth_source_plugin.py,sha256=PcoW-23hVg0WB5u-T2TmmdOrYdF_zkgtmsavsKcpg_o,1861
3
+ auth_source/authenticator.py,sha256=o9OElnlNOsGfUwjs42QKV3qd-aPf_ayH0wCk9M2jlIA,1467
4
+ auth_source/config.py,sha256=gWwM3iw29A2C5nHNtHtxVJh7Kp6hCfk4Um_c80EJMoc,3137
5
+ auth_source/installer.py,sha256=LrxtkpMI-ubmlaPZKazhbuh8bDlDxRhHOduDwvi_cys,1539
6
+ auth_source/source_credentials.py,sha256=YduTSilKECR6I33eF6OIpUVNVNqmfmolW7PikhKhqkc,1639
7
+ auth_source/types.py,sha256=e7GuJF5X0XKHFzfS0ADoq95x6ncQ5oGeBHdW2OOIOuA,848
8
+ auth_source_extensions/plugins/auth_source.py,sha256=9xok3PYVQVub1bA-slPksvd5U7lQseLolHPt4tqlePc,1102
9
+ conan_auth_source_plugin-0.0.5.dist-info/licenses/LICENSE,sha256=P9a82Ofi3oMxuvQ85tZT84Un2FYz9yHndiwP2XIBzqQ,1117
10
+ conan_auth_source_plugin-0.0.5.dist-info/METADATA,sha256=9vFHhBlrY6fHzJhi3ySrI3z7PT9szViMJuYBD-gbZf8,6316
11
+ conan_auth_source_plugin-0.0.5.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
12
+ conan_auth_source_plugin-0.0.5.dist-info/entry_points.txt,sha256=rzPCZZ5__pTwem1OZmaJ6Ufk0BoKZa3igq36rc0NkrA,80
13
+ conan_auth_source_plugin-0.0.5.dist-info/top_level.txt,sha256=uYNHnySICBwxjdmitoWBFvHVSfesW-1YGhZWxPB7SkM,35
14
+ conan_auth_source_plugin-0.0.5.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ conan-auth-source-plugin-install = auth_source.installer:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Utilties, plugins and extesions for Conan package management
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ auth_source
2
+ auth_source_extensions