apigeecli 0.53.3__tar.gz → 0.53.4__tar.gz
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.
- {apigeecli-0.53.3 → apigeecli-0.53.4}/LICENSE +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/NOTICE +0 -0
- {apigeecli-0.53.3/apigeecli.egg-info → apigeecli-0.53.4}/PKG-INFO +34 -13
- {apigeecli-0.53.3 → apigeecli-0.53.4}/README.rst +29 -11
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/__init__.py +3 -3
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/__main__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apiproducts/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apiproducts/apiproducts.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apiproducts/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apiproducts/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apis/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apis/api_puller_with_dependency_extraction.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apis/apis.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apis/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apis/deploy.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apis/pull.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apis/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apps/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apps/apps.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apps/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/apps/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/auth.py +40 -52
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/backups/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/backups/backups.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/backups/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/caches/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/caches/caches.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/caches/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/caches/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/cls.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/configure/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/configure/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/console.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/crypto.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/deployments/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/deployments/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/deployments/deployments.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/deployments/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/developers/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/developers/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/developers/developers.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/developers/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/encryption_utils.py +12 -14
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/exceptions.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/keystores/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/keystores/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/keystores/keystores.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/keystores/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/keyvaluemaps/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/keyvaluemaps/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/keyvaluemaps/keyvaluemaps.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/keyvaluemaps/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/maskconfigs/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/maskconfigs/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/maskconfigs/maskconfigs.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/maskconfigs/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/permissions/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/permissions/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/permissions/permissions.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/permissions/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/plugins/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/plugins/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/prefix.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/references/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/references/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/references/references.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/references/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/sharedflows/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/sharedflows/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/sharedflows/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/sharedflows/sharedflows.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/silent.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/targetservers/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/targetservers/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/targetservers/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/targetservers/targetservers.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/types.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/userroles/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/userroles/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/userroles/userroles.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/utils.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/utils_init.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/verbose.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/virtualhosts/__init__.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/virtualhosts/commands.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/virtualhosts/serializer.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigee/virtualhosts/virtualhosts.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4/apigeecli.egg-info}/PKG-INFO +34 -13
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigeecli.egg-info/SOURCES.txt +4 -1
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigeecli.egg-info/dependency_links.txt +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigeecli.egg-info/entry_points.txt +1 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigeecli.egg-info/requires.txt +3 -3
- {apigeecli-0.53.3 → apigeecli-0.53.4}/apigeecli.egg-info/top_level.txt +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/setup.cfg +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/setup.py +0 -0
- {apigeecli-0.53.3 → apigeecli-0.53.4}/tests/__init__.py +0 -0
- apigeecli-0.53.4/tests/test_auth.py +63 -0
- apigeecli-0.53.4/tests/test_echo.py +35 -0
- apigeecli-0.53.4/tests/test_encryption_utils.py +32 -0
|
File without changes
|
|
File without changes
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: apigeecli
|
|
3
|
-
Version: 0.53.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.53.4
|
|
4
|
+
Summary: The Apigee Edge command-line interface is an unofficial Python command-line tool built to simplify and automate Apigee Edge API usage, with support for SSO, MFA, and basic authentication.
|
|
5
5
|
Home-page: https://github.com/darumatic/apigee-cli
|
|
6
6
|
Author: Matthew Delotavo
|
|
7
7
|
Author-email: matthew.t.delotavo@gmail.com
|
|
8
8
|
License: Apache license 2.0
|
|
9
9
|
Project-URL: Documentation, https://darumatic.github.io/apigee-cli/index.html
|
|
10
|
+
Platform: UNKNOWN
|
|
10
11
|
Classifier: Development Status :: 4 - Beta
|
|
11
12
|
Classifier: Environment :: Console
|
|
12
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -22,23 +23,23 @@ License-File: NOTICE
|
|
|
22
23
|
apigee-cli
|
|
23
24
|
==========
|
|
24
25
|
|
|
25
|
-
⚠️ **Warning:** This project is no longer actively maintained and was created specifically for previous Darumatic clients.
|
|
26
|
-
|
|
27
26
|
|License| |Python version| |Downloads|
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
.. warning::
|
|
29
|
+
|
|
30
|
+
This tool is no longer actively maintained.
|
|
31
|
+
|
|
32
|
+
The Apigee Edge command-line interface is an unofficial Python command-line tool built to simplify and automate Apigee Edge API usage, with support for SSO, MFA, and basic authentication.
|
|
33
|
+
|
|
34
|
+
Initially developed for Darumatic clients, this project remains available as a working proof of concept, proven reliable in production CI/CD pipelines. The custom plugins feature will remain functional even if Apigee Edge APIs stop working.
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
and to support automation for common development tasks such as CI/CD.
|
|
36
|
+
I have created private forks for clients, where I continued to add features, fixes, and unit tests. However, I do not intend to maintain this public version. Feel free to explore and learn from the code.
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
or command-line arguments.
|
|
38
|
+
If you’re up for a challenge, try updating the code to work with Apigee X :)
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
.. note::
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
refer to the documentation and links provided.
|
|
42
|
+
**apigee-cli** is highly experimental and is not affiliated with Apigee or Google.
|
|
42
43
|
|
|
43
44
|
------------
|
|
44
45
|
Installation
|
|
@@ -144,6 +145,24 @@ We built and use the Apigee CLI to implement and distribute features that allow
|
|
|
144
145
|
to manage CI/CD, perform self-service operations and promote our DevOps workflows
|
|
145
146
|
which are difficult to perform using official tools.
|
|
146
147
|
|
|
148
|
+
-------------
|
|
149
|
+
Running Tests
|
|
150
|
+
-------------
|
|
151
|
+
|
|
152
|
+
This project uses `unittest` for testing its codebase. In order to run the tests, you will need to install the `coverage.py` tool. You can install it using pip:
|
|
153
|
+
|
|
154
|
+
.. code-block:: bash
|
|
155
|
+
|
|
156
|
+
pip3 install coverage
|
|
157
|
+
|
|
158
|
+
Once `coverage.py` is installed, you can run the tests using the `runtests` script:
|
|
159
|
+
|
|
160
|
+
.. code-block:: bash
|
|
161
|
+
|
|
162
|
+
./runtests
|
|
163
|
+
|
|
164
|
+
This script will run all the tests in the `tests` directory and generate a coverage report.
|
|
165
|
+
|
|
147
166
|
------------
|
|
148
167
|
Getting Help
|
|
149
168
|
------------
|
|
@@ -224,3 +243,5 @@ This is not an officially supported Google product.
|
|
|
224
243
|
|
|
225
244
|
.. _`Apigee CI/CD Docker releases`: https://hub.docker.com/r/darumatic/apigee-cicd
|
|
226
245
|
|
|
246
|
+
|
|
247
|
+
|
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
apigee-cli
|
|
3
3
|
==========
|
|
4
4
|
|
|
5
|
-
⚠️ **Warning:** This project is no longer actively maintained and was created specifically for previous Darumatic clients.
|
|
6
|
-
|
|
7
5
|
|License| |Python version| |Downloads|
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
.. warning::
|
|
8
|
+
|
|
9
|
+
This tool is no longer actively maintained.
|
|
10
|
+
|
|
11
|
+
The Apigee Edge command-line interface is an unofficial Python command-line tool built to simplify and automate Apigee Edge API usage, with support for SSO, MFA, and basic authentication.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
and to support automation for common development tasks such as CI/CD.
|
|
13
|
+
Initially developed for Darumatic clients, this project remains available as a working proof of concept, proven reliable in production CI/CD pipelines. The custom plugins feature will remain functional even if Apigee Edge APIs stop working.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
or command-line arguments.
|
|
15
|
+
I have created private forks for clients, where I continued to add features, fixes, and unit tests. However, I do not intend to maintain this public version. Feel free to explore and learn from the code.
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
If you’re up for a challenge, try updating the code to work with Apigee X :)
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
.. note::
|
|
20
|
+
|
|
21
|
+
**apigee-cli** is highly experimental and is not affiliated with Apigee or Google.
|
|
22
22
|
|
|
23
23
|
------------
|
|
24
24
|
Installation
|
|
@@ -124,6 +124,24 @@ We built and use the Apigee CLI to implement and distribute features that allow
|
|
|
124
124
|
to manage CI/CD, perform self-service operations and promote our DevOps workflows
|
|
125
125
|
which are difficult to perform using official tools.
|
|
126
126
|
|
|
127
|
+
-------------
|
|
128
|
+
Running Tests
|
|
129
|
+
-------------
|
|
130
|
+
|
|
131
|
+
This project uses `unittest` for testing its codebase. In order to run the tests, you will need to install the `coverage.py` tool. You can install it using pip:
|
|
132
|
+
|
|
133
|
+
.. code-block:: bash
|
|
134
|
+
|
|
135
|
+
pip3 install coverage
|
|
136
|
+
|
|
137
|
+
Once `coverage.py` is installed, you can run the tests using the `runtests` script:
|
|
138
|
+
|
|
139
|
+
.. code-block:: bash
|
|
140
|
+
|
|
141
|
+
./runtests
|
|
142
|
+
|
|
143
|
+
This script will run all the tests in the `tests` directory and generate a coverage report.
|
|
144
|
+
|
|
127
145
|
------------
|
|
128
146
|
Getting Help
|
|
129
147
|
------------
|
|
@@ -6,9 +6,9 @@ from apigee import utils_init
|
|
|
6
6
|
|
|
7
7
|
APP = "apigeecli"
|
|
8
8
|
CMD = "apigee"
|
|
9
|
-
__version__ = "0.53.
|
|
10
|
-
description = "
|
|
11
|
-
long_description = """
|
|
9
|
+
__version__ = "0.53.4"
|
|
10
|
+
description = "The Apigee Edge command-line interface is an unofficial Python command-line tool built to simplify and automate Apigee Edge API usage, with support for SSO, MFA, and basic authentication."
|
|
11
|
+
long_description = """This tool is no longer actively maintained. Initially developed for Darumatic clients, this project remains available as a working proof of concept, proven reliable in production CI/CD pipelines. The custom plugins feature will remain functional even if Apigee Edge APIs stop working."""
|
|
12
12
|
|
|
13
13
|
APIGEE_CLI_DIRECTORY = utils_init.join_path_components(Path.home(), ".apigee")
|
|
14
14
|
APIGEE_CLI_PLUGINS_DIRECTORY = utils_init.join_path_components(APIGEE_CLI_DIRECTORY, "plugins")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -37,21 +37,21 @@ def attach_is_token_option(func, profile):
|
|
|
37
37
|
"--token/--no-token",
|
|
38
38
|
default=is_token,
|
|
39
39
|
show_default=True,
|
|
40
|
-
help="specify to use
|
|
40
|
+
help="specify to use OAuth without MFA",
|
|
41
41
|
)(func)
|
|
42
42
|
elif is_token_envvar in (True, "True", "true", "1"):
|
|
43
43
|
func = click.option(
|
|
44
44
|
"--token/--no-token",
|
|
45
45
|
default=is_token_envvar,
|
|
46
46
|
show_default=True,
|
|
47
|
-
help="specify to use
|
|
47
|
+
help="specify to use OAuth without MFA",
|
|
48
48
|
)(func)
|
|
49
49
|
else:
|
|
50
50
|
func = click.option(
|
|
51
51
|
"--token/--no-token",
|
|
52
52
|
default=False,
|
|
53
53
|
show_default=True,
|
|
54
|
-
help="specify to use
|
|
54
|
+
help="specify to use OAuth without MFA",
|
|
55
55
|
)(func)
|
|
56
56
|
return func
|
|
57
57
|
|
|
@@ -222,75 +222,68 @@ def get_access_token_for_mfa(
|
|
|
222
222
|
)
|
|
223
223
|
|
|
224
224
|
|
|
225
|
-
def get_access_token(auth, username, password, oauth_url, post_headers, session):
|
|
225
|
+
def get_access_token(auth, username, password, oauth_url, post_headers, session, passcode):
|
|
226
226
|
if auth.token or APIGEE_CLI_IS_MACHINE_USER:
|
|
227
227
|
return get_access_token_for_token(auth, username, password, oauth_url, post_headers, session)
|
|
228
228
|
elif auth.mfa_secret:
|
|
229
229
|
return get_access_token_for_mfa(auth, username, password, oauth_url, post_headers, session)
|
|
230
230
|
elif auth.zonename:
|
|
231
|
-
return get_access_token_for_sso(auth, username, password, oauth_url, post_headers, session)
|
|
231
|
+
return get_access_token_for_sso(auth, username, password, oauth_url, post_headers, session, passcode)
|
|
232
232
|
|
|
233
233
|
|
|
234
234
|
def get_access_token_for_sso(
|
|
235
|
-
auth, username, password, oauth_url, post_headers, session
|
|
235
|
+
auth, username, password, oauth_url, post_headers, session, passcode
|
|
236
236
|
):
|
|
237
237
|
refresh_token = validate_refresh_token(auth)
|
|
238
238
|
oauth_url = APIGEE_ZONENAME_OAUTH_URL.format(zonename=auth.zonename)
|
|
239
239
|
passcode_url = APIGEE_SAML_LOGIN_URL.format(zonename=auth.zonename)
|
|
240
|
+
|
|
240
241
|
if not refresh_token:
|
|
241
|
-
|
|
242
|
+
if not passcode:
|
|
243
|
+
passcode = get_sso_temporary_authentication_code(passcode_url)
|
|
244
|
+
post_body = f"passcode={passcode}&grant_type=password&response_type=token"
|
|
242
245
|
else:
|
|
243
246
|
# Should we notify users that the refresh token is being used to verify the access token?
|
|
244
247
|
#console.echo("Refresh Token found, renewing access token with Refresh Token...")
|
|
245
248
|
post_body = f"grant_type=refresh_token&refresh_token={refresh_token}"
|
|
246
249
|
|
|
247
250
|
try:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
251
|
+
response_post = session.post(
|
|
252
|
+
f"{oauth_url}", headers=post_headers, data=post_body
|
|
253
|
+
)
|
|
254
|
+
response_data = response_post.json()
|
|
255
|
+
response_data["access_token"]
|
|
256
|
+
|
|
257
|
+
# If we didn't have a refresh token previously, save the refresh token we just got.
|
|
258
|
+
if not refresh_token:
|
|
259
|
+
with open(APIGEE_CLI_REFRESH_TOKEN_FILE, "w") as f:
|
|
260
|
+
f.write(response_data["refresh_token"])
|
|
261
|
+
return response_data
|
|
262
|
+
except KeyError:
|
|
263
|
+
sys.exit("Temporary Authentication Code or Refresh Token is invalid. Please try again.")
|
|
261
264
|
except ConnectionError as ce:
|
|
262
265
|
console.echo(ce)
|
|
263
|
-
except KeyError:
|
|
264
|
-
pass
|
|
265
266
|
|
|
266
267
|
|
|
267
268
|
def get_config_value(config_section, config_key):
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if
|
|
269
|
+
config = configparser.ConfigParser()
|
|
270
|
+
config.read(APIGEE_CLI_CREDENTIALS_FILE)
|
|
271
|
+
if config_section in config:
|
|
272
|
+
if config_key in config[config_section]:
|
|
272
273
|
return config[config_section][config_key]
|
|
273
|
-
except Exception:
|
|
274
|
-
return
|
|
275
274
|
|
|
276
275
|
|
|
277
|
-
def
|
|
276
|
+
def get_sso_temporary_authentication_code(passcode_url):
|
|
278
277
|
webbrowser.open(passcode_url)
|
|
279
|
-
console.echo(
|
|
280
|
-
|
|
281
|
-
)
|
|
282
|
-
console.echo(
|
|
283
|
-
"Follow the instructions in the browser to complete this authorization request."
|
|
284
|
-
)
|
|
285
|
-
console.echo(
|
|
286
|
-
f"""\nIf your browser did not automatically open, go to the following URL and sign in:\n\n{passcode_url}\n\nthen copy the Temporary Authentication Code.\n"""
|
|
287
|
-
)
|
|
278
|
+
console.echo("SSO authorization page has automatically been opened in your default browser.")
|
|
279
|
+
console.echo("Follow the instructions in the browser to complete this authorization request.")
|
|
280
|
+
console.echo(f"""\nIf your browser did not automatically open, go to the following URL and sign in:\n\n{passcode_url}\n\nthen copy the Temporary Authentication Code.\n""")
|
|
288
281
|
|
|
289
282
|
passcode = click.prompt("Please enter the Temporary Authentication Code")
|
|
290
|
-
return
|
|
283
|
+
return passcode
|
|
291
284
|
|
|
292
285
|
|
|
293
|
-
def retrieve_access_token(authentication,
|
|
286
|
+
def retrieve_access_token(authentication, passcode=None):
|
|
294
287
|
oauth_url = APIGEE_OAUTH_URL
|
|
295
288
|
username = authentication.username
|
|
296
289
|
password = authentication.password
|
|
@@ -302,7 +295,7 @@ def retrieve_access_token(authentication, session=None):
|
|
|
302
295
|
"Accept": "application/json;charset=utf-8",
|
|
303
296
|
"Authorization": "Basic ZWRnZWNsaTplZGdlY2xpc2VjcmV0",
|
|
304
297
|
}
|
|
305
|
-
response_data = get_access_token(authentication, username, password, oauth_url, post_headers, session)
|
|
298
|
+
response_data = get_access_token(authentication, username, password, oauth_url, post_headers, session, passcode)
|
|
306
299
|
try:
|
|
307
300
|
return response_data["access_token"]
|
|
308
301
|
except KeyError as error:
|
|
@@ -366,19 +359,18 @@ def auth():
|
|
|
366
359
|
pass
|
|
367
360
|
|
|
368
361
|
|
|
369
|
-
@auth.command(name="get-access-token", help="
|
|
362
|
+
@auth.command(name="get-access-token", help="Request a fresh access token")
|
|
370
363
|
@common_auth_options
|
|
371
364
|
@common_verbose_options
|
|
372
365
|
@common_silent_options
|
|
366
|
+
@click.option('--passcode', help='Apigee SAML temporary authentication code to use when getting an Apigee access token (optional)')
|
|
373
367
|
def get_access_token_command(
|
|
374
|
-
username, password, mfa_secret, token, zonename, org, profile, **kwargs
|
|
368
|
+
username, password, mfa_secret, token, zonename, org, profile, passcode, **kwargs
|
|
375
369
|
):
|
|
376
|
-
console.echo(
|
|
377
|
-
retrieve_access_token(generate_authentication(username, password, mfa_secret, token, zonename))
|
|
378
|
-
)
|
|
370
|
+
console.echo(retrieve_access_token(generate_authentication(username, password, mfa_secret, token, zonename), passcode))
|
|
379
371
|
|
|
380
372
|
|
|
381
|
-
@auth.command(help="
|
|
373
|
+
@auth.command(help="View the current access token")
|
|
382
374
|
@common_auth_options
|
|
383
375
|
@common_verbose_options
|
|
384
376
|
@common_silent_options
|
|
@@ -392,11 +384,7 @@ def view_access_token(
|
|
|
392
384
|
console.echo(validate_access_token(authentication_object))
|
|
393
385
|
else:
|
|
394
386
|
# Show the user/password base64 basic auth value
|
|
395
|
-
console.echo(
|
|
396
|
-
base64.b64encode(
|
|
397
|
-
f"{authentication_object.username}:{authentication_object.password}".encode()
|
|
398
|
-
).decode()
|
|
399
|
-
)
|
|
387
|
+
console.echo(base64.b64encode(f"{authentication_object.username}:{authentication_object.password}".encode()).decode())
|
|
400
388
|
|
|
401
389
|
|
|
402
390
|
@auth.command(help="Clear cached access token and refresh token")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
|
|
3
2
|
import gnupg
|
|
4
3
|
|
|
5
4
|
ENCRYPTED_HEADER_BEGIN = "-----BEGIN ENCRYPTED APIGEE CLI MESSAGE-----"
|
|
@@ -8,22 +7,20 @@ ENCRYPTED_HEADER_END = "-----END ENCRYPTED APIGEE CLI MESSAGE-----"
|
|
|
8
7
|
|
|
9
8
|
def encrypt_message_with_gpg(secret, message, encoded=True):
|
|
10
9
|
gpg = gnupg.GPG()
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
str(
|
|
14
|
-
gpg.encrypt(
|
|
15
|
-
message, symmetric="AES256", passphrase=secret, recipients=None
|
|
16
|
-
)
|
|
17
|
-
).encode()
|
|
18
|
-
).decode()
|
|
19
|
-
return str(
|
|
20
|
-
gpg.encrypt(message, symmetric="AES256", passphrase=secret, recipients=None)
|
|
10
|
+
encrypted_message = gpg.encrypt(
|
|
11
|
+
message, symmetric="AES256", passphrase=secret, recipients=None
|
|
21
12
|
)
|
|
13
|
+
if encoded:
|
|
14
|
+
return base64.b64encode(str(encrypted_message).encode()).decode()
|
|
15
|
+
return str(encrypted_message)
|
|
22
16
|
|
|
23
17
|
|
|
24
18
|
def has_encrypted_header(message):
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
if not isinstance(message, str):
|
|
20
|
+
return False
|
|
21
|
+
return (
|
|
22
|
+
message.startswith(ENCRYPTED_HEADER_BEGIN)
|
|
23
|
+
and message.endswith(ENCRYPTED_HEADER_END)
|
|
27
24
|
)
|
|
28
25
|
|
|
29
26
|
|
|
@@ -33,5 +30,6 @@ def decrypt_message_with_gpg(secret, message, encoded=True):
|
|
|
33
30
|
return ""
|
|
34
31
|
message = message[len(ENCRYPTED_HEADER_BEGIN) : -len(ENCRYPTED_HEADER_END)]
|
|
35
32
|
if encoded:
|
|
36
|
-
|
|
33
|
+
decoded_message = base64.b64decode(message).decode()
|
|
34
|
+
return str(gpg.decrypt(decoded_message, passphrase=secret))
|
|
37
35
|
return str(gpg.decrypt(message, passphrase=secret))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: apigeecli
|
|
3
|
-
Version: 0.53.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.53.4
|
|
4
|
+
Summary: The Apigee Edge command-line interface is an unofficial Python command-line tool built to simplify and automate Apigee Edge API usage, with support for SSO, MFA, and basic authentication.
|
|
5
5
|
Home-page: https://github.com/darumatic/apigee-cli
|
|
6
6
|
Author: Matthew Delotavo
|
|
7
7
|
Author-email: matthew.t.delotavo@gmail.com
|
|
8
8
|
License: Apache license 2.0
|
|
9
9
|
Project-URL: Documentation, https://darumatic.github.io/apigee-cli/index.html
|
|
10
|
+
Platform: UNKNOWN
|
|
10
11
|
Classifier: Development Status :: 4 - Beta
|
|
11
12
|
Classifier: Environment :: Console
|
|
12
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -22,23 +23,23 @@ License-File: NOTICE
|
|
|
22
23
|
apigee-cli
|
|
23
24
|
==========
|
|
24
25
|
|
|
25
|
-
⚠️ **Warning:** This project is no longer actively maintained and was created specifically for previous Darumatic clients.
|
|
26
|
-
|
|
27
26
|
|License| |Python version| |Downloads|
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
.. warning::
|
|
29
|
+
|
|
30
|
+
This tool is no longer actively maintained.
|
|
31
|
+
|
|
32
|
+
The Apigee Edge command-line interface is an unofficial Python command-line tool built to simplify and automate Apigee Edge API usage, with support for SSO, MFA, and basic authentication.
|
|
33
|
+
|
|
34
|
+
Initially developed for Darumatic clients, this project remains available as a working proof of concept, proven reliable in production CI/CD pipelines. The custom plugins feature will remain functional even if Apigee Edge APIs stop working.
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
and to support automation for common development tasks such as CI/CD.
|
|
36
|
+
I have created private forks for clients, where I continued to add features, fixes, and unit tests. However, I do not intend to maintain this public version. Feel free to explore and learn from the code.
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
or command-line arguments.
|
|
38
|
+
If you’re up for a challenge, try updating the code to work with Apigee X :)
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
.. note::
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
refer to the documentation and links provided.
|
|
42
|
+
**apigee-cli** is highly experimental and is not affiliated with Apigee or Google.
|
|
42
43
|
|
|
43
44
|
------------
|
|
44
45
|
Installation
|
|
@@ -144,6 +145,24 @@ We built and use the Apigee CLI to implement and distribute features that allow
|
|
|
144
145
|
to manage CI/CD, perform self-service operations and promote our DevOps workflows
|
|
145
146
|
which are difficult to perform using official tools.
|
|
146
147
|
|
|
148
|
+
-------------
|
|
149
|
+
Running Tests
|
|
150
|
+
-------------
|
|
151
|
+
|
|
152
|
+
This project uses `unittest` for testing its codebase. In order to run the tests, you will need to install the `coverage.py` tool. You can install it using pip:
|
|
153
|
+
|
|
154
|
+
.. code-block:: bash
|
|
155
|
+
|
|
156
|
+
pip3 install coverage
|
|
157
|
+
|
|
158
|
+
Once `coverage.py` is installed, you can run the tests using the `runtests` script:
|
|
159
|
+
|
|
160
|
+
.. code-block:: bash
|
|
161
|
+
|
|
162
|
+
./runtests
|
|
163
|
+
|
|
164
|
+
This script will run all the tests in the `tests` directory and generate a coverage report.
|
|
165
|
+
|
|
147
166
|
------------
|
|
148
167
|
Getting Help
|
|
149
168
|
------------
|
|
@@ -224,3 +243,5 @@ This is not an officially supported Google product.
|
|
|
224
243
|
|
|
225
244
|
.. _`Apigee CI/CD Docker releases`: https://hub.docker.com/r/darumatic/apigee-cicd
|
|
226
245
|
|
|
246
|
+
|
|
247
|
+
|
|
@@ -91,4 +91,7 @@ apigeecli.egg-info/dependency_links.txt
|
|
|
91
91
|
apigeecli.egg-info/entry_points.txt
|
|
92
92
|
apigeecli.egg-info/requires.txt
|
|
93
93
|
apigeecli.egg-info/top_level.txt
|
|
94
|
-
tests/__init__.py
|
|
94
|
+
tests/__init__.py
|
|
95
|
+
tests/test_auth.py
|
|
96
|
+
tests/test_echo.py
|
|
97
|
+
tests/test_encryption_utils.py
|
|
File without changes
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
GitPython==3.1.30
|
|
2
|
+
PyJWT==2.6.0
|
|
2
3
|
click-aliases==1.0.1
|
|
3
4
|
click-option-group==0.5.5
|
|
5
|
+
click==8.1.3
|
|
4
6
|
colorama==0.4.6
|
|
5
7
|
coverage==7.0.1
|
|
6
|
-
GitPython==3.1.30
|
|
7
8
|
pudb==2022.1.3
|
|
8
|
-
PyJWT==2.6.0
|
|
9
9
|
pyotp==2.8.0
|
|
10
10
|
python-gnupg<0.5.0,>=0.3.5
|
|
11
11
|
requests==2.28.1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
|
|
4
|
+
from apigee.auth import get_access_token_for_sso
|
|
5
|
+
|
|
6
|
+
class TestAuth(unittest.TestCase):
|
|
7
|
+
def setUp(self):
|
|
8
|
+
self.auth = MagicMock()
|
|
9
|
+
self.username = "test_user"
|
|
10
|
+
self.password = "test_password"
|
|
11
|
+
self.oauth_url = "https://example.com/oauth"
|
|
12
|
+
self.post_headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
13
|
+
self.session = MagicMock()
|
|
14
|
+
|
|
15
|
+
@patch("apigee.auth.validate_refresh_token")
|
|
16
|
+
@patch("apigee.auth.get_sso_temporary_authentication_code")
|
|
17
|
+
@patch("builtins.open")
|
|
18
|
+
def test_get_access_token_for_sso_no_refresh_token(self, mock_open, mock_get_sso_temporary_authentication_code, mock_validate_refresh_token):
|
|
19
|
+
mock_validate_refresh_token.return_value = None
|
|
20
|
+
mock_get_sso_temporary_authentication_code.return_value = "test_passcode"
|
|
21
|
+
|
|
22
|
+
response_data = {
|
|
23
|
+
"access_token": "test_access_token",
|
|
24
|
+
"refresh_token": "test_refresh_token"
|
|
25
|
+
}
|
|
26
|
+
self.session.post.return_value.json.return_value = response_data
|
|
27
|
+
|
|
28
|
+
result = get_access_token_for_sso(
|
|
29
|
+
self.auth, self.username, self.password, self.oauth_url, self.post_headers, self.session, None
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
mock_get_sso_temporary_authentication_code.assert_called_once_with(f"https://{self.auth.zonename}.login.apigee.com/passcode")
|
|
33
|
+
self.session.post.assert_called_once_with(
|
|
34
|
+
f"https://{self.auth.zonename}.login.apigee.com/oauth/token", headers=self.post_headers, data="passcode=test_passcode&grant_type=password&response_type=token"
|
|
35
|
+
)
|
|
36
|
+
self.assertEqual(result, response_data)
|
|
37
|
+
# mock_open.assert_not_called()
|
|
38
|
+
mock_open.assert_called_once_with('/home/mdelotavo/.apigee/refresh_token', 'w')
|
|
39
|
+
|
|
40
|
+
@patch("apigee.auth.validate_refresh_token")
|
|
41
|
+
@patch("builtins.open")
|
|
42
|
+
def test_get_access_token_for_sso_with_refresh_token(self, mock_open, mock_validate_refresh_token):
|
|
43
|
+
mock_validate_refresh_token.return_value = "test_refresh_token"
|
|
44
|
+
|
|
45
|
+
response_data = {
|
|
46
|
+
"access_token": "test_access_token",
|
|
47
|
+
"refresh_token": "test_refresh_token"
|
|
48
|
+
}
|
|
49
|
+
self.session.post.return_value.json.return_value = response_data
|
|
50
|
+
|
|
51
|
+
result = get_access_token_for_sso(
|
|
52
|
+
self.auth, self.username, self.password, self.oauth_url, self.post_headers, self.session, None
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self.session.post.assert_called_once_with(
|
|
56
|
+
f"https://{self.auth.zonename}.login.apigee.com/oauth/token", headers=self.post_headers, data="grant_type=refresh_token&refresh_token=test_refresh_token"
|
|
57
|
+
)
|
|
58
|
+
self.assertEqual(result, response_data)
|
|
59
|
+
# mock_open.assert_called_once_with('/root/.apigee/refresh_token', 'w')
|
|
60
|
+
mock_open.assert_not_called()
|
|
61
|
+
|
|
62
|
+
if __name__ == '__main__':
|
|
63
|
+
unittest.main()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
from apigee.console import echo
|
|
4
|
+
|
|
5
|
+
class TestEcho(unittest.TestCase):
|
|
6
|
+
@patch('builtins.print')
|
|
7
|
+
@patch('sys.exit')
|
|
8
|
+
def test_echo_silent(self, mock_exit, mock_print):
|
|
9
|
+
echo('Hello', make_silent=True, exit_status=1)
|
|
10
|
+
mock_print.assert_not_called()
|
|
11
|
+
mock_exit.assert_called_once_with(1)
|
|
12
|
+
|
|
13
|
+
@patch('builtins.print')
|
|
14
|
+
@patch('sys.exit')
|
|
15
|
+
def test_echo_not_silent(self, mock_exit, mock_print):
|
|
16
|
+
echo('Hello', make_silent=False)
|
|
17
|
+
mock_print.assert_called_once_with('Hello', end='\n', flush=False)
|
|
18
|
+
mock_exit.assert_not_called()
|
|
19
|
+
|
|
20
|
+
@patch('builtins.print')
|
|
21
|
+
@patch('sys.exit')
|
|
22
|
+
def test_echo_verbosity(self, mock_exit, mock_print):
|
|
23
|
+
echo('Hello', current_verbosity=1, expected_verbosity=0)
|
|
24
|
+
mock_print.assert_called_once_with('Hello', end='\n', flush=False)
|
|
25
|
+
mock_exit.assert_not_called()
|
|
26
|
+
|
|
27
|
+
@patch('builtins.print')
|
|
28
|
+
@patch('sys.exit')
|
|
29
|
+
def test_echo_no_verbosity(self, mock_exit, mock_print):
|
|
30
|
+
echo('Hello', current_verbosity=0, expected_verbosity=1)
|
|
31
|
+
mock_print.assert_not_called()
|
|
32
|
+
mock_exit.assert_not_called()
|
|
33
|
+
|
|
34
|
+
if __name__ == '__main__':
|
|
35
|
+
unittest.main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from apigee.encryption_utils import (ENCRYPTED_HEADER_BEGIN, ENCRYPTED_HEADER_END,
|
|
4
|
+
decrypt_message_with_gpg, encrypt_message_with_gpg)
|
|
5
|
+
|
|
6
|
+
class TestEncryptionUtils(unittest.TestCase):
|
|
7
|
+
ciphertext = None
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def setUpClass(cls):
|
|
11
|
+
cls.secret = "password123!"
|
|
12
|
+
cls.plaintext = "Hello, World!"
|
|
13
|
+
cls.ciphertext = f"{ENCRYPTED_HEADER_BEGIN}{encrypt_message_with_gpg(cls.secret, cls.plaintext)}{ENCRYPTED_HEADER_END}"
|
|
14
|
+
|
|
15
|
+
def test_decrypt_message_with_gpg_encoded(self):
|
|
16
|
+
result = decrypt_message_with_gpg(self.secret, self.ciphertext, encoded=True)
|
|
17
|
+
self.assertEqual(result, self.plaintext)
|
|
18
|
+
|
|
19
|
+
def test_decrypt_message_with_gpg_encoded_plaintext_none(self):
|
|
20
|
+
result = decrypt_message_with_gpg(self.secret, None, encoded=True)
|
|
21
|
+
self.assertEqual(result, "")
|
|
22
|
+
|
|
23
|
+
def test_decrypt_message_with_gpg_encoded_plaintext_empty(self):
|
|
24
|
+
result = decrypt_message_with_gpg(self.secret, "", encoded=True)
|
|
25
|
+
self.assertEqual(result, "")
|
|
26
|
+
|
|
27
|
+
def test_decrypt_message_with_gpg_no_encrypted_header(self):
|
|
28
|
+
result = decrypt_message_with_gpg(self.secret, self.plaintext)
|
|
29
|
+
self.assertEqual(result, "")
|
|
30
|
+
|
|
31
|
+
if __name__ == '__main__':
|
|
32
|
+
unittest.main()
|