vgs-cli 0.0.1.dev0__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.
- vgs_cli-0.0.1.dev0.data/data/vgscli/calm.yaml +16 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/checkout.yaml +21 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/http-route-template.yaml +61 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/mft-route-template.yaml +10 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/payments-admin.yaml +25 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/service-account-schema.yaml +54 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/sub-account-checkout.yaml +23 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vault-resources.yaml +710 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vault-schema.yaml +36 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vault-template.yaml +12 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vgs-cli.yaml +17 -0
- vgs_cli-0.0.1.dev0.dist-info/METADATA +139 -0
- vgs_cli-0.0.1.dev0.dist-info/RECORD +56 -0
- vgs_cli-0.0.1.dev0.dist-info/WHEEL +5 -0
- vgs_cli-0.0.1.dev0.dist-info/entry_points.txt +2 -0
- vgs_cli-0.0.1.dev0.dist-info/licenses/LICENSE +22 -0
- vgs_cli-0.0.1.dev0.dist-info/top_level.txt +1 -0
- vgscli/__init__.py +0 -0
- vgscli/_version.py +32 -0
- vgscli/access_logs.py +65 -0
- vgscli/audits_api.py +102 -0
- vgscli/auth.py +68 -0
- vgscli/auth_server.py +131 -0
- vgscli/auth_utils.py +24 -0
- vgscli/callback_server.py +41 -0
- vgscli/cert_manager_api.py +34 -0
- vgscli/cli/__init__.py +23 -0
- vgscli/cli/commands/__init__.py +3 -0
- vgscli/cli/commands/apply.py +307 -0
- vgscli/cli/commands/generate.py +134 -0
- vgscli/cli/commands/get.py +200 -0
- vgscli/cli/types/__init__.py +2 -0
- vgscli/cli/types/resource_id.py +39 -0
- vgscli/cli/types/variable.py +21 -0
- vgscli/cli_utils.py +132 -0
- vgscli/click_extensions.py +88 -0
- vgscli/config_file.py +58 -0
- vgscli/errors.py +263 -0
- vgscli/file_token_util.py +30 -0
- vgscli/id_generator.py +46 -0
- vgscli/keyring_token_util.py +128 -0
- vgscli/resource-templates/http-route-template.yaml +61 -0
- vgscli/resource-templates/mft-route-template.yaml +10 -0
- vgscli/resource-templates/service-account/calm.yaml +16 -0
- vgscli/resource-templates/service-account/checkout.yaml +21 -0
- vgscli/resource-templates/service-account/payments-admin.yaml +25 -0
- vgscli/resource-templates/service-account/sub-account-checkout.yaml +23 -0
- vgscli/resource-templates/service-account/vgs-cli.yaml +17 -0
- vgscli/resource-templates/vault-template.yaml +12 -0
- vgscli/testing.py +48 -0
- vgscli/text.py +9 -0
- vgscli/token_handler.py +11 -0
- vgscli/validation-schemas/service-account-schema.yaml +54 -0
- vgscli/validation-schemas/vault-resources.yaml +710 -0
- vgscli/validation-schemas/vault-schema.yaml +36 -0
- vgscli/vgs.py +249 -0
vgscli/errors.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
import types
|
|
3
|
+
from functools import wraps
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from click import Context, UsageError
|
|
7
|
+
from click.exceptions import Abort, Exit
|
|
8
|
+
from simple_rest_client import exceptions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_debug(ctx) -> bool:
|
|
12
|
+
return bool(
|
|
13
|
+
getattr(ctx, "obj", None) and getattr(getattr(ctx, "obj", None), "debug", False)
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_error_details(e):
|
|
18
|
+
resp = getattr(e, "response", None)
|
|
19
|
+
body = getattr(resp, "body", None)
|
|
20
|
+
if isinstance(body, dict) and body.get("errors"):
|
|
21
|
+
return [x.get("detail") for x in body["errors"] if isinstance(x, dict)]
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VgsCliError(UsageError):
|
|
26
|
+
def __init__(self, message, ctx=None):
|
|
27
|
+
self.message = message
|
|
28
|
+
self.ctx = ctx
|
|
29
|
+
|
|
30
|
+
def show(self, file=None):
|
|
31
|
+
self.show_message()
|
|
32
|
+
|
|
33
|
+
def show_message(self):
|
|
34
|
+
debug = is_debug(self.ctx)
|
|
35
|
+
if debug:
|
|
36
|
+
click.echo(traceback.format_exc(), err=True)
|
|
37
|
+
click.echo(self.message, err=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class VaultNotFoundError(VgsCliError):
|
|
41
|
+
def __init__(self, vault_id, ctx=None):
|
|
42
|
+
self.message = "Vault " + vault_id + " doesn't exist."
|
|
43
|
+
self.ctx = ctx
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TokenNotValidError(VgsCliError):
|
|
47
|
+
def __init__(self, message=None, ctx=None):
|
|
48
|
+
self.message = (
|
|
49
|
+
message or "Please run `vgs login` because your session has been expired."
|
|
50
|
+
)
|
|
51
|
+
self.ctx = ctx
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AuthenticationRequiredError(VgsCliError):
|
|
55
|
+
def __init__(self, ctx=None):
|
|
56
|
+
self.message = "Please run `vgs login` because your session has been expired."
|
|
57
|
+
self.ctx = ctx
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AuthenticationError(VgsCliError):
|
|
61
|
+
def __init__(self, ctx=None, details=None):
|
|
62
|
+
debug = is_debug(ctx)
|
|
63
|
+
self.message = (
|
|
64
|
+
"Authentication error occurred"
|
|
65
|
+
if debug
|
|
66
|
+
else "Authentication error occurred."
|
|
67
|
+
)
|
|
68
|
+
if details:
|
|
69
|
+
self.message = "{message} {details}".format(
|
|
70
|
+
message=self.message, details=details
|
|
71
|
+
)
|
|
72
|
+
self.ctx = ctx
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ClientCredentialsAuthenticationError(VgsCliError):
|
|
76
|
+
def __init__(self, ctx=None):
|
|
77
|
+
debug = is_debug(ctx)
|
|
78
|
+
self.message = (
|
|
79
|
+
"""Authentication error occurred.
|
|
80
|
+
|
|
81
|
+
NOTE: You may see this error if you're using an account with enabled OTP.
|
|
82
|
+
Auto login via environment variables with OTP is not supported.
|
|
83
|
+
"""
|
|
84
|
+
if debug
|
|
85
|
+
else """Authentication error occurred. (Run with --debug for a traceback.)
|
|
86
|
+
|
|
87
|
+
NOTE: You may see this error if you're using an account with enabled OTP.
|
|
88
|
+
Auto login via environment variables with OTP is not supported.
|
|
89
|
+
"""
|
|
90
|
+
)
|
|
91
|
+
self.ctx = ctx
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ApiError(VgsCliError):
|
|
95
|
+
def __init__(self, e, ctx=None):
|
|
96
|
+
details = get_error_details(e)
|
|
97
|
+
debug = is_debug(ctx)
|
|
98
|
+
if details:
|
|
99
|
+
self.message = f"API error occurred: {details}"
|
|
100
|
+
elif debug:
|
|
101
|
+
self.message = "API error occurred."
|
|
102
|
+
else:
|
|
103
|
+
self.message = "API error occurred. (Run with --debug for a traceback.)"
|
|
104
|
+
self.ctx = ctx
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class UnhandledError(VgsCliError):
|
|
108
|
+
def __init__(self, ctx=None):
|
|
109
|
+
debug = is_debug(ctx)
|
|
110
|
+
self.message = (
|
|
111
|
+
"An unexpected error occurred."
|
|
112
|
+
if debug
|
|
113
|
+
else "An unexpected error occurred. (Run with --debug for a traceback.)"
|
|
114
|
+
)
|
|
115
|
+
self.ctx = ctx
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RouteNotValidError(VgsCliError):
|
|
119
|
+
def __init__(self, error_message: str, ctx=None):
|
|
120
|
+
debug = is_debug(ctx)
|
|
121
|
+
error_message = f"Route cannot be applied due to errors:\n{error_message}"
|
|
122
|
+
self.message = (
|
|
123
|
+
error_message
|
|
124
|
+
if debug
|
|
125
|
+
else f"{error_message} (Run with --debug for a traceback.)"
|
|
126
|
+
)
|
|
127
|
+
self.ctx = ctx
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class SchemaValidationError(VgsCliError):
|
|
131
|
+
def __init__(self, message, ctx=None):
|
|
132
|
+
self.message = f"Error during validation of the file input: {message}."
|
|
133
|
+
self.ctx = ctx
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ServiceClientCreationError(VgsCliError):
|
|
137
|
+
def __init__(self, e, ctx=None):
|
|
138
|
+
details = get_error_details(e)
|
|
139
|
+
self.message = f"Service Account creation failed with error: {details}"
|
|
140
|
+
self.ctx = ctx
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ServiceClientDeletionError(VgsCliError):
|
|
144
|
+
def __init__(self, e, ctx=None):
|
|
145
|
+
details = get_error_details(e)
|
|
146
|
+
self.message = f"Service Account deletion failed with error: {details}"
|
|
147
|
+
self.ctx = ctx
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ServiceClientListingError(VgsCliError):
|
|
151
|
+
def __init__(self, e, ctx=None):
|
|
152
|
+
details = get_error_details(e)
|
|
153
|
+
self.message = f"Service Account listing failed with error: {details}"
|
|
154
|
+
self.ctx = ctx
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class NoSuchFileOrDirectoryError(VgsCliError):
|
|
158
|
+
def __init__(self, message, ctx=None):
|
|
159
|
+
self.message = f"No such file or directory: {message}"
|
|
160
|
+
self.ctx = ctx
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class InsufficientPermissionsError(VgsCliError):
|
|
164
|
+
def __init__(self, ctx=None):
|
|
165
|
+
self.message = (
|
|
166
|
+
"You don't have enough permissions to perform this operation. Please contact "
|
|
167
|
+
"support@verygoodsecurity.com. "
|
|
168
|
+
)
|
|
169
|
+
self.ctx = ctx
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class PermissionDeniedError(VgsCliError):
|
|
173
|
+
def __init__(self, ctx=None):
|
|
174
|
+
self.message = "You don't have enough permissions to the requested resource"
|
|
175
|
+
self.ctx = ctx
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class MaxNumberOfCredentialsExceededError(VgsCliError):
|
|
179
|
+
def __init__(self, ctx=None):
|
|
180
|
+
self.message = (
|
|
181
|
+
"Maximum number of access credentials (10 per vault) reached.. Please contact "
|
|
182
|
+
"support@verygoodsecurity.com to increase the limit. "
|
|
183
|
+
)
|
|
184
|
+
self.ctx = ctx
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def status_code_error(**kwargs):
|
|
188
|
+
def init(self, e, ctx=None):
|
|
189
|
+
status = getattr(getattr(e, "response", None), "status_code", None)
|
|
190
|
+
error_handler = kwargs.get(f"e{status}")
|
|
191
|
+
if error_handler:
|
|
192
|
+
self.message = (
|
|
193
|
+
error_handler(e)
|
|
194
|
+
if isinstance(error_handler, types.FunctionType)
|
|
195
|
+
else error_handler
|
|
196
|
+
)
|
|
197
|
+
self.ctx = ctx
|
|
198
|
+
else:
|
|
199
|
+
ApiError.__init__(self, e, ctx=ctx)
|
|
200
|
+
|
|
201
|
+
return type(
|
|
202
|
+
"StatusCodeError",
|
|
203
|
+
(ApiError,),
|
|
204
|
+
{"__init__": init},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def handle_errors(**outer_kwargs):
|
|
209
|
+
def decorator_handle_errors(f):
|
|
210
|
+
@wraps(f)
|
|
211
|
+
def wrapper(*args, **kwargs):
|
|
212
|
+
try:
|
|
213
|
+
return f(*args, **kwargs)
|
|
214
|
+
except (Abort, Exit):
|
|
215
|
+
raise
|
|
216
|
+
except exceptions.ClientError as e:
|
|
217
|
+
ctx = args[0] if args and isinstance(args[0], Context) else None
|
|
218
|
+
if ctx:
|
|
219
|
+
status = getattr(getattr(e, "response", None), "status_code", None)
|
|
220
|
+
if status == 401:
|
|
221
|
+
raise outer_kwargs.get(
|
|
222
|
+
"TokenNotValidError", TokenNotValidError
|
|
223
|
+
)(ctx=ctx)
|
|
224
|
+
if status == 403:
|
|
225
|
+
body = getattr(getattr(e, "response", None), "body", None)
|
|
226
|
+
errors = body.get("errors") if isinstance(body, dict) else None
|
|
227
|
+
detail = (
|
|
228
|
+
(
|
|
229
|
+
errors
|
|
230
|
+
and isinstance(errors, list)
|
|
231
|
+
and errors
|
|
232
|
+
and errors[0].get("detail")
|
|
233
|
+
)
|
|
234
|
+
if errors
|
|
235
|
+
else None
|
|
236
|
+
)
|
|
237
|
+
if detail and "Maximum number of access credentials" in detail:
|
|
238
|
+
raise outer_kwargs.get(
|
|
239
|
+
"MaxNumberOfCredentialsExceededError",
|
|
240
|
+
MaxNumberOfCredentialsExceededError,
|
|
241
|
+
)(ctx=ctx)
|
|
242
|
+
if errors:
|
|
243
|
+
raise outer_kwargs.get(
|
|
244
|
+
"InsufficientPermissionsError",
|
|
245
|
+
InsufficientPermissionsError,
|
|
246
|
+
)(ctx=ctx)
|
|
247
|
+
raise outer_kwargs.get(
|
|
248
|
+
"PermissionDeniedError", PermissionDeniedError
|
|
249
|
+
)(ctx=ctx)
|
|
250
|
+
raise outer_kwargs.get("ApiError", ApiError)(e, ctx=ctx)
|
|
251
|
+
# No click.Context available; re-raise original error
|
|
252
|
+
raise
|
|
253
|
+
except (VgsCliError, click.BadParameter, click.BadArgumentUsage):
|
|
254
|
+
raise
|
|
255
|
+
except Exception:
|
|
256
|
+
ctx = args[0] if args and isinstance(args[0], Context) else None
|
|
257
|
+
if ctx:
|
|
258
|
+
raise UnhandledError(ctx=ctx)
|
|
259
|
+
raise
|
|
260
|
+
|
|
261
|
+
return wrapper
|
|
262
|
+
|
|
263
|
+
return decorator_handle_errors
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from vgs.sdk.utils import is_file_accessible
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileTokenUtil:
|
|
9
|
+
def __init__(self, token_file_name):
|
|
10
|
+
temp_dir = tempfile.gettempdir()
|
|
11
|
+
self.temp_file = os.path.join(temp_dir, token_file_name)
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def exists(self):
|
|
15
|
+
return os.path.exists(self.temp_file)
|
|
16
|
+
|
|
17
|
+
def read_token(self):
|
|
18
|
+
with open(self.temp_file, "r+") as tmp:
|
|
19
|
+
return tmp.read()
|
|
20
|
+
|
|
21
|
+
def write_token(self, token):
|
|
22
|
+
with open(self.temp_file, "w+") as tmp:
|
|
23
|
+
tmp.write(token)
|
|
24
|
+
return token
|
|
25
|
+
|
|
26
|
+
def remove_token(self):
|
|
27
|
+
if is_file_accessible(self.temp_file, "r+"):
|
|
28
|
+
os.remove(self.temp_file)
|
|
29
|
+
else:
|
|
30
|
+
click.echo("No authenticated session", err=True)
|
vgscli/id_generator.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Optional,
|
|
3
|
+
Union,
|
|
4
|
+
)
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
import base58
|
|
8
|
+
|
|
9
|
+
BASE58_ALPHABET = b"123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def base58_to_uuid(public_id: str, prefix: Optional[str] = None) -> str:
|
|
13
|
+
"""
|
|
14
|
+
>>> base58_to_uuid('fBpkbaCp2dqfowSaRDL3g7')
|
|
15
|
+
'765159e7-750c-4b0b-8be0-a07fbc8f3ac4'
|
|
16
|
+
>>> base58_to_uuid('ACgwBwv1PeYLXpxsMyrSKYbw', 'AC')
|
|
17
|
+
'7dbf495e-254b-4e76-aaab-1bb5b726a53a'
|
|
18
|
+
>>> base58_to_uuid('bPpQJd2cwkVewGNBtCYMC')
|
|
19
|
+
'0182a7e1-d2e9-48dc-805b-d79156802ce6'
|
|
20
|
+
>>> base58_to_uuid('3iEhRdf9YZCNViqA1FRU6')
|
|
21
|
+
'00525efa-b7f3-41c4-b315-8d1836c9ec89'
|
|
22
|
+
"""
|
|
23
|
+
prefix = prefix or ""
|
|
24
|
+
return str(
|
|
25
|
+
UUID(
|
|
26
|
+
int=base58.b58decode_int(public_id[len(prefix) :], alphabet=BASE58_ALPHABET)
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def uuid_to_base58(internal_id: Union[UUID, str], prefix: Optional[str] = None) -> str:
|
|
32
|
+
"""
|
|
33
|
+
>>> uuid_to_base58('765159e7-750c-4b0b-8be0-a07fbc8f3ac4', 'GR')
|
|
34
|
+
'GRfBpkbaCp2dqfowSaRDL3g7'
|
|
35
|
+
>>> uuid_to_base58('7dbf495e-254b-4e76-aaab-1bb5b726a53a')
|
|
36
|
+
'gwBwv1PeYLXpxsMyrSKYbw'
|
|
37
|
+
>>> uuid_to_base58('0182a7e1-d2e9-48dc-805b-d79156802ce6', 'GR')
|
|
38
|
+
'GRbPpQJd2cwkVewGNBtCYMC'
|
|
39
|
+
>>> uuid_to_base58('00525efa-b7f3-41c4-b315-8d1836c9ec89', 'GR')
|
|
40
|
+
'GR3iEhRdf9YZCNViqA1FRU6'
|
|
41
|
+
"""
|
|
42
|
+
prefix = prefix or ""
|
|
43
|
+
uuid = internal_id if isinstance(internal_id, UUID) else UUID(hex=internal_id)
|
|
44
|
+
encoded = base58.b58encode_int(uuid.int, alphabet=BASE58_ALPHABET)
|
|
45
|
+
|
|
46
|
+
return f"{prefix}{encoded.decode()}"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import jwt
|
|
2
|
+
import keyring
|
|
3
|
+
from cryptography.fernet import Fernet
|
|
4
|
+
from keyring.errors import PasswordDeleteError
|
|
5
|
+
from vgs.sdk.utils import expired
|
|
6
|
+
|
|
7
|
+
from vgscli.errors import TokenNotValidError
|
|
8
|
+
from vgscli.file_token_util import FileTokenUtil
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KeyringTokenUtil:
|
|
12
|
+
SERVICE_NAME = "vgs-cli"
|
|
13
|
+
|
|
14
|
+
ACCESS_TOKEN_KEY = "access_token"
|
|
15
|
+
REFRESH_TOKEN_KEY = "refresh_token"
|
|
16
|
+
ENCRYPT_TOKEN_SECRET_KEY = "vgs_encrypt_secret_key"
|
|
17
|
+
|
|
18
|
+
access_token_file = FileTokenUtil("vgs_access_token")
|
|
19
|
+
refresh_token_file = FileTokenUtil("vgs_refresh_token")
|
|
20
|
+
|
|
21
|
+
def put_encryption_secret(self, secret):
|
|
22
|
+
keyring.set_password(self.SERVICE_NAME, self.ENCRYPT_TOKEN_SECRET_KEY, secret)
|
|
23
|
+
|
|
24
|
+
def get_encryption_secret(self):
|
|
25
|
+
return keyring.get_credential(self.SERVICE_NAME, self.ENCRYPT_TOKEN_SECRET_KEY)
|
|
26
|
+
|
|
27
|
+
def remove_encryption_secret(self):
|
|
28
|
+
try:
|
|
29
|
+
keyring.delete_password(self.SERVICE_NAME, self.ENCRYPT_TOKEN_SECRET_KEY)
|
|
30
|
+
except PasswordDeleteError:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def tokens_exist(self):
|
|
34
|
+
return self.access_token_file.exists and self.refresh_token_file.exists
|
|
35
|
+
|
|
36
|
+
def clear_tokens(self):
|
|
37
|
+
self.delete_access_token()
|
|
38
|
+
self.delete_refresh_token()
|
|
39
|
+
|
|
40
|
+
def validate_access_token(self):
|
|
41
|
+
if self.get_access_token():
|
|
42
|
+
token_json = jwt.decode(
|
|
43
|
+
self.get_access_token(), options={"verify_signature": False}
|
|
44
|
+
)
|
|
45
|
+
return not expired(token_json["exp"])
|
|
46
|
+
else:
|
|
47
|
+
raise TokenNotValidError("Access token not found")
|
|
48
|
+
|
|
49
|
+
def validate_refresh_token(self):
|
|
50
|
+
if self.get_refresh_token():
|
|
51
|
+
token_json = jwt.decode(
|
|
52
|
+
self.get_refresh_token(), options={"verify_signature": False}
|
|
53
|
+
)
|
|
54
|
+
if expired(token_json["exp"]):
|
|
55
|
+
raise TokenNotValidError("Refresh token expired")
|
|
56
|
+
else:
|
|
57
|
+
raise TokenNotValidError("Refresh token not found")
|
|
58
|
+
|
|
59
|
+
def is_access_token_valid(self) -> bool:
|
|
60
|
+
try:
|
|
61
|
+
return self.validate_access_token()
|
|
62
|
+
except TokenNotValidError:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def is_access_token_azp_changed(self, azp) -> bool:
|
|
66
|
+
return (
|
|
67
|
+
jwt.decode(
|
|
68
|
+
self.get_access_token(), options={"verify_signature": False}
|
|
69
|
+
).get("azp")
|
|
70
|
+
!= azp
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def put_tokens(self, response):
|
|
74
|
+
key = Fernet.generate_key()
|
|
75
|
+
cipher = Fernet(key)
|
|
76
|
+
self.remove_encryption_secret()
|
|
77
|
+
self.put_encryption_secret(str(key, "utf-8"))
|
|
78
|
+
self.access_token_file.write_token(
|
|
79
|
+
str(cipher.encrypt(response[self.ACCESS_TOKEN_KEY].encode()), "utf-8")
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.3
|
|
83
|
+
if self.REFRESH_TOKEN_KEY in response:
|
|
84
|
+
self.refresh_token_file.write_token(
|
|
85
|
+
str(cipher.encrypt(response[self.REFRESH_TOKEN_KEY].encode()), "utf-8")
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def put_access_token(self, token):
|
|
89
|
+
self.access_token_file.write_token(
|
|
90
|
+
str(self.fernet.encrypt(token.encode()), "utf-8")
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def get_access_token(self):
|
|
94
|
+
try:
|
|
95
|
+
return str(
|
|
96
|
+
self.fernet.decrypt(self.access_token_file.read_token().encode()),
|
|
97
|
+
"utf-8",
|
|
98
|
+
)
|
|
99
|
+
except FileNotFoundError:
|
|
100
|
+
raise TokenNotValidError("Access token not found")
|
|
101
|
+
|
|
102
|
+
def delete_access_token(self):
|
|
103
|
+
self.access_token_file.remove_token()
|
|
104
|
+
|
|
105
|
+
def put_refresh_token(self, token):
|
|
106
|
+
self.refresh_token_file.write_token(
|
|
107
|
+
str(self.fernet.encrypt(token.encode()), "utf-8")
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def get_refresh_token(self):
|
|
111
|
+
try:
|
|
112
|
+
return str(
|
|
113
|
+
self.fernet.decrypt(self.refresh_token_file.read_token().encode()),
|
|
114
|
+
"utf-8",
|
|
115
|
+
)
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
raise TokenNotValidError("Refresh token not found")
|
|
118
|
+
|
|
119
|
+
def delete_refresh_token(self):
|
|
120
|
+
self.refresh_token_file.remove_token()
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def fernet(self):
|
|
124
|
+
secret = self.get_encryption_secret()
|
|
125
|
+
if not secret:
|
|
126
|
+
raise FileNotFoundError("Can't find secret in keystore")
|
|
127
|
+
|
|
128
|
+
return Fernet(secret.password.encode())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
apiVersion: vault.vgs.io/v1
|
|
2
|
+
kind: HttpRoute
|
|
3
|
+
metadata:
|
|
4
|
+
name: your-name-here
|
|
5
|
+
labels:
|
|
6
|
+
vgs.io/vaultId: your-vault-id-here
|
|
7
|
+
spec:
|
|
8
|
+
# TODO: Change this ID
|
|
9
|
+
id: &routeId 7478d3b7-beef-cafe-0000-000000000000
|
|
10
|
+
type: rule_chain
|
|
11
|
+
attributes:
|
|
12
|
+
id: *routeId
|
|
13
|
+
host_endpoint: httpbin.org
|
|
14
|
+
destination_override_endpoint: '*'
|
|
15
|
+
ordinal: 0
|
|
16
|
+
port: 0
|
|
17
|
+
protocol: http
|
|
18
|
+
source_endpoint: '*'
|
|
19
|
+
tags:
|
|
20
|
+
# TODO: add a good name
|
|
21
|
+
name: Display name of route here
|
|
22
|
+
# TODO: add version here
|
|
23
|
+
vgs.io/version: 0.1.0
|
|
24
|
+
# filters
|
|
25
|
+
entries:
|
|
26
|
+
# first filter - document here what it does
|
|
27
|
+
- classifiers: {}
|
|
28
|
+
config:
|
|
29
|
+
condition: OR
|
|
30
|
+
rules:
|
|
31
|
+
- expression:
|
|
32
|
+
field: PathInfo
|
|
33
|
+
operator: matches
|
|
34
|
+
type: string
|
|
35
|
+
values:
|
|
36
|
+
- /post
|
|
37
|
+
id: 955834e8-beef-cafe-0000-000000000000
|
|
38
|
+
id_selector: null
|
|
39
|
+
operation: ENRICH
|
|
40
|
+
operations:
|
|
41
|
+
- name: github.com/verygoodsecurity/common/compute/larky/http/Process
|
|
42
|
+
parameters:
|
|
43
|
+
script: |
|
|
44
|
+
load('@stdlib//json', 'json')
|
|
45
|
+
load("@stdlib//builtins", "builtins")
|
|
46
|
+
|
|
47
|
+
load("@vgs//vault", "vault")
|
|
48
|
+
|
|
49
|
+
def process(input, ctx):
|
|
50
|
+
# TODO: write your larky code here.
|
|
51
|
+
|
|
52
|
+
return input
|
|
53
|
+
phase: REQUEST
|
|
54
|
+
public_token_generator: UUID
|
|
55
|
+
targets:
|
|
56
|
+
- body
|
|
57
|
+
token_manager: PERSISTENT
|
|
58
|
+
transformer: JSON_PATH
|
|
59
|
+
transformer_config:
|
|
60
|
+
- $.whatever_this_field_is_unused
|
|
61
|
+
transformer_config_map: null
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
apiVersion: 1.0.0
|
|
2
|
+
kind: ServiceAccount
|
|
3
|
+
data:
|
|
4
|
+
name: calm
|
|
5
|
+
{%- if vaults | length == 0 %}
|
|
6
|
+
{{ cli_fail("This template needs single vault to be specified. Please use '--vault <vault-identifier>' to pass the vault.") }}
|
|
7
|
+
{% elif vaults | length == 1 %}
|
|
8
|
+
vaults:
|
|
9
|
+
- {{ vaults[0] }}
|
|
10
|
+
{%- else %}
|
|
11
|
+
{{ cli_fail("This template doesn't allow multiple vaults.") }}
|
|
12
|
+
{% endif %}
|
|
13
|
+
scopes:
|
|
14
|
+
- name: cards:write
|
|
15
|
+
- name: network-tokens:write
|
|
16
|
+
- name: merchants:write
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
apiVersion: 1.0.0
|
|
2
|
+
kind: ServiceAccount
|
|
3
|
+
data:
|
|
4
|
+
name: {{ name }}
|
|
5
|
+
{%- if vaults | length == 0 %}
|
|
6
|
+
{{ cli_fail("This template needs single vault to be specified. Please use '--vault <vault-identifier>' to pass the vault.") }}
|
|
7
|
+
{% elif vaults | length == 1 %}
|
|
8
|
+
vaults:
|
|
9
|
+
- {{ vaults[0] }}
|
|
10
|
+
{%- else %}
|
|
11
|
+
{{ cli_fail("This template doesn't allow multiple vaults.") }}
|
|
12
|
+
{% endif %}
|
|
13
|
+
scopes:
|
|
14
|
+
- name: financial-instruments:write
|
|
15
|
+
optional: true
|
|
16
|
+
- name: transfers:write
|
|
17
|
+
optional: true
|
|
18
|
+
- name: orders:write
|
|
19
|
+
optional: true
|
|
20
|
+
|
|
21
|
+
accessTokenLifespan: 28800
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
apiVersion: 1.0.0
|
|
2
|
+
kind: ServiceAccount
|
|
3
|
+
data:
|
|
4
|
+
{%- if vaults | length == 0 %}
|
|
5
|
+
{{ cli_fail("This template needs single vault to be specified. Please use '--vault <vault-identifier>' to pass the vault.") }}
|
|
6
|
+
{%- elif vaults | length == 1 %}
|
|
7
|
+
name: payments-{{ vaults[0] }}
|
|
8
|
+
vaults:
|
|
9
|
+
- {{ vaults[0] }}
|
|
10
|
+
{%- else %}
|
|
11
|
+
{{ cli_fail("This template doesn't allow multiple vaults.") }}
|
|
12
|
+
{% endif %}
|
|
13
|
+
scopes:
|
|
14
|
+
- name: financial-instruments:admin
|
|
15
|
+
- name: gateways:admin
|
|
16
|
+
- name: rules:admin
|
|
17
|
+
- name: transfers:admin
|
|
18
|
+
- name: orders:admin
|
|
19
|
+
- name: threeds:admin
|
|
20
|
+
- name: sub-accounts:admin
|
|
21
|
+
- name: cards:write
|
|
22
|
+
- name: network-tokens:write
|
|
23
|
+
- name: merchants:write
|
|
24
|
+
|
|
25
|
+
accessTokenLifespan: 28800
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
apiVersion: 1.0.0
|
|
2
|
+
kind: ServiceAccount
|
|
3
|
+
data:
|
|
4
|
+
name: {{ sub_account_id }}
|
|
5
|
+
annotations:
|
|
6
|
+
"vgs.io/sub-account": "{{ sub_account_id }}"
|
|
7
|
+
{%- if vaults | length == 0 %}
|
|
8
|
+
{{ cli_fail("This template needs single vault to be specified. Please use '--vault <vault-identifier>' to pass the vault.") }}
|
|
9
|
+
{% elif vaults | length == 1 %}
|
|
10
|
+
vaults:
|
|
11
|
+
- {{ vaults[0] }}
|
|
12
|
+
{%- else %}
|
|
13
|
+
{{ cli_fail("This template doesn't allow multiple vaults.") }}
|
|
14
|
+
{% endif %}
|
|
15
|
+
scopes:
|
|
16
|
+
- name: financial-instruments:write
|
|
17
|
+
optional: true
|
|
18
|
+
- name: transfers:write
|
|
19
|
+
optional: true
|
|
20
|
+
- name: orders:write
|
|
21
|
+
optional: true
|
|
22
|
+
|
|
23
|
+
accessTokenLifespan: 28800
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
apiVersion: 1.0.0
|
|
2
|
+
kind: ServiceAccount
|
|
3
|
+
data:
|
|
4
|
+
name: vgs-cli
|
|
5
|
+
{%- if vaults | length == 0 %}
|
|
6
|
+
{{- cli_warn("Service Account won't have access to any vaults inside organization. If you need it to access vault(s) please use --vault <vault-identifier>.") or "" }}
|
|
7
|
+
{%- else %}
|
|
8
|
+
vaults:
|
|
9
|
+
{%- for vault_id in vaults %}
|
|
10
|
+
- {{ vault_id }}
|
|
11
|
+
{%- endfor %}
|
|
12
|
+
{%- endif %}
|
|
13
|
+
scopes:
|
|
14
|
+
- name: access-logs:read
|
|
15
|
+
- name: organizations:read
|
|
16
|
+
- name: routes:write
|
|
17
|
+
- name: vaults:write
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
apiVersion: 1.0.0
|
|
2
|
+
kind: Vault
|
|
3
|
+
data:
|
|
4
|
+
# Name of the vault to create. (Must be between 3 and 50 characters long.)
|
|
5
|
+
name: Very Good Vault
|
|
6
|
+
|
|
7
|
+
# Environment to create the vault within.
|
|
8
|
+
# See https://www.verygoodsecurity.com/docs/terminology/vaults#environments
|
|
9
|
+
environment: SANDBOX
|
|
10
|
+
|
|
11
|
+
# ID of the organization to associate the vault with.
|
|
12
|
+
organizationId: AC6mGNNRecR7K5N7AjX5niz4
|