Skip to content

Commit 26f3ac8

Browse files
MCOL-5019: review fixes.
1 parent 628a471 commit 26f3ac8

File tree

7 files changed

+230
-56
lines changed

7 files changed

+230
-56
lines changed

cmapi/cmapi_server/handlers/cej.py

+67-38
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
1111
from cryptography.hazmat.primitives import padding
1212

13-
from cmapi_server.constants import (
14-
MCS_DATA_PATH, MCS_SECRETS_FILENAME, MCS_SECRETS_FILE_PATH
15-
)
13+
from cmapi_server.constants import MCS_DATA_PATH, MCS_SECRETS_FILENAME
1614
from cmapi_server.exceptions import CEJError
1715

1816

@@ -26,16 +24,19 @@ class CEJPasswordHandler():
2624
"""Handler for CrossEngineSupport password decryption."""
2725

2826
@classmethod
29-
def secretsfile_exists(cls) -> bool:
30-
"""Check the .secrets file in MCS_SECRETS_FILE_PATH.
27+
def secretsfile_exists(cls, directory: str = MCS_DATA_PATH) -> bool:
28+
"""Check the .secrets file in directory. Default MCS_SECRETS_FILE_PATH.
3129
30+
:param directory: path to the directory with .secrets file
31+
:type directory: str, optional
3232
:return: True if file exists and not empty.
3333
:rtype: bool
3434
"""
35+
secrets_file_path = os.path.join(directory, MCS_SECRETS_FILENAME)
3536
try:
3637
if (
37-
os.path.isfile(MCS_SECRETS_FILE_PATH) and
38-
os.path.getsize(MCS_SECRETS_FILE_PATH) > 0
38+
os.path.isfile(secrets_file_path) and
39+
os.path.getsize(secrets_file_path) > 0
3940
):
4041
return True
4142
except Exception:
@@ -49,40 +50,48 @@ def secretsfile_exists(cls) -> bool:
4950
return False
5051

5152
@classmethod
52-
def get_secrets_json(cls) -> dict:
53+
def get_secrets_json(cls, directory: str = MCS_DATA_PATH) -> dict:
5354
"""Get json from .secrets file.
5455
55-
:raises CEJError: on empty\corrupted\wrong format .secrets file
56+
:param directory: path to the directory with .secrets file
57+
:type directory: str, optional
5658
:return: json from .secrets file
5759
:rtype: dict
60+
:raises CEJError: on empty\corrupted\wrong format .secrets file
5861
"""
59-
if not cls.secretsfile_exists():
60-
raise CEJError(f'{MCS_SECRETS_FILE_PATH} file does not exist.')
61-
with open(MCS_SECRETS_FILE_PATH) as secrets_file:
62+
secrets_file_path = os.path.join(directory, MCS_SECRETS_FILENAME)
63+
if not cls.secretsfile_exists(directory=directory):
64+
raise CEJError(f'{secrets_file_path} file does not exist.')
65+
with open(secrets_file_path) as secrets_file:
6266
try:
6367
secrets_json = json.load(secrets_file)
6468
except Exception:
6569
logging.error(
6670
'Something went wrong while loading json from '
67-
f'{MCS_SECRETS_FILE_PATH}',
71+
f'{secrets_file_path}',
6872
exc_info=True
6973
)
7074
raise CEJError(
71-
f'Looks like file {MCS_SECRETS_FILE_PATH} is corrupted or'
75+
f'Looks like file {secrets_file_path} is corrupted or'
7276
'has wrong format.'
7377
) from None
7478
return secrets_json
7579

7680
@classmethod
77-
def decrypt_password(cls, enc_data: str) -> str:
81+
def decrypt_password(
82+
cls, enc_data: str, directory: str = MCS_DATA_PATH
83+
) -> str:
7884
"""Decrypt CEJ password if needed.
7985
86+
:param directory: path to the directory with .secrets file
87+
:type directory: str, optional
8088
:param enc_data: encrypted initialization vector + password in hex str
8189
:type enc_data: str
8290
:return: decrypted CEJ password
8391
:rtype: str
8492
"""
85-
if not cls.secretsfile_exists():
93+
secrets_file_path = os.path.join(directory, MCS_SECRETS_FILENAME)
94+
if not cls.secretsfile_exists(directory=directory):
8695
logging.warning('Unencrypted CrossEngineSupport password used.')
8796
return enc_data
8897

@@ -96,18 +105,18 @@ def decrypt_password(cls, enc_data: str) -> str:
96105
'Non-hexadecimal number found in encrypted CEJ password.'
97106
) from value_error
98107

99-
secrets_json = cls.get_secrets_json()
108+
secrets_json = cls.get_secrets_json(directory=directory)
100109
encryption_key_hex = secrets_json.get('encryption_key', None)
101110
if not encryption_key_hex:
102111
raise CEJError(
103-
f'Empty "encryption key" found in {MCS_SECRETS_FILE_PATH}'
112+
f'Empty "encryption key" found in {secrets_file_path}'
104113
)
105114
try:
106115
encryption_key = bytes.fromhex(encryption_key_hex)
107116
except ValueError as value_error:
108117
raise CEJError(
109118
'Non-hexadecimal number found in encryption key from '
110-
f'{MCS_SECRETS_FILE_PATH} file.'
119+
f'{secrets_file_path} file.'
111120
) from value_error
112121
cipher = Cipher(
113122
algorithms.AES(encryption_key),
@@ -125,21 +134,34 @@ def decrypt_password(cls, enc_data: str) -> str:
125134
return passwd_bytes.decode()
126135

127136
@classmethod
128-
def encrypt_password(cls, passwd: str) -> str:
129-
iv = os.urandom(size=AES_IV_BIN_SIZE)
137+
def encrypt_password(
138+
cls, passwd: str, directory: str = MCS_DATA_PATH
139+
) -> str:
140+
"""Encrypt CEJ password.
141+
142+
:param directory: path to the directory with .secrets file
143+
:type directory: str, optional
144+
:param passwd: CEJ password
145+
:type passwd: str
146+
:return: encrypted CEJ password in uppercase hex format
147+
:rtype: str
148+
"""
130149

131-
secrets_json = cls.get_secrets_json()
150+
secrets_file_path = os.path.join(directory, MCS_SECRETS_FILENAME)
151+
iv = os.urandom(AES_IV_BIN_SIZE)
152+
153+
secrets_json = cls.get_secrets_json(directory=directory)
132154
encryption_key_hex = secrets_json.get('encryption_key')
133155
if not encryption_key_hex:
134156
raise CEJError(
135-
f'Empty "encryption key" found in {MCS_SECRETS_FILE_PATH}'
157+
f'Empty "encryption key" found in {secrets_file_path}'
136158
)
137159
try:
138160
encryption_key = bytes.fromhex(encryption_key_hex)
139161
except ValueError as value_error:
140162
raise CEJError(
141163
'Non-hexadecimal number found in encryption key from '
142-
f'{MCS_SECRETS_FILE_PATH} file.'
164+
f'{secrets_file_path} file.'
143165
) from value_error
144166
cipher = Cipher(
145167
algorithms.AES(encryption_key),
@@ -151,8 +173,8 @@ def encrypt_password(cls, passwd: str) -> str:
151173
padded_data = padder.update(passwd.encode()) + padder.finalize()
152174

153175
encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
154-
155-
return iv + encrypted_data
176+
encrypted_passwd_bytes = iv + encrypted_data
177+
return encrypted_passwd_bytes.hex().upper()
156178

157179
@classmethod
158180
def generate_secrets_data(cls) -> dict:
@@ -161,8 +183,8 @@ def generate_secrets_data(cls) -> dict:
161183
:return: secrets data
162184
:rtype: dict
163185
"""
164-
key_length = algorithms.AES256.key_size // 8
165-
encryption_key = os.urandom(size=key_length)
186+
key_length = 32 # AES256 key_size
187+
encryption_key = os.urandom(key_length)
166188
encryption_key_hex = binascii.hexlify(encryption_key).decode()
167189
secrets_dict = {
168190
'description': 'Columnstore CrossEngineSupport password encryption/decryption key',
@@ -174,41 +196,48 @@ def generate_secrets_data(cls) -> dict:
174196

175197
@classmethod
176198
def save_secrets(
177-
cls, secrets: dict, filepath: str = MCS_SECRETS_FILE_PATH,
199+
cls, secrets: dict, directory: str = MCS_DATA_PATH,
178200
owner: str = 'mysql'
179201
) -> None:
180202
"""Write secrets to .secrets file.
181203
204+
:param directory: path to the directory with .secrets file
205+
:type directory: str, optional
182206
:param secrets: secrets dict
183207
:type secrets: dict
184208
:param filepath: path to the .secrets file
185209
:type filepath: str, optional
186210
:param owner: owner of the file
187211
:type owner: str, optional
188212
"""
189-
if cls.secretsfile_exists():
213+
secrets_file_path = os.path.join(directory, MCS_SECRETS_FILENAME)
214+
if cls.secretsfile_exists(directory=directory):
190215
copyfile(
191-
filepath,
216+
secrets_file_path,
192217
os.path.join(
193-
os.path.dirname(filepath),
194-
f'{os.path.basename(filepath)}.cmapi.save'
218+
directory,
219+
f'{MCS_SECRETS_FILENAME}.cmapi.save'
195220
)
196221
)
197222

198223
try:
199224
with open(
200-
MCS_SECRETS_FILE_PATH, 'w', encoding='utf-8'
225+
secrets_file_path, 'w', encoding='utf-8'
201226
) as secrets_file:
202227
json.dump(secrets, secrets_file)
203228
except Exception as exc:
204229
raise CEJError(f'Write to .secrets file failed.') from exc
205230

206231
try:
207-
os.chmod(MCS_SECRETS_FILE_PATH, stat.S_IRUSR)
232+
os.chmod(secrets_file_path, stat.S_IRUSR)
208233
userinfo = pwd.getpwnam(owner)
209-
os.chown(MCS_SECRETS_FILE_PATH, userinfo.pw_uid, userinfo.pw_gid)
210-
logging.debug(f'Permissions of .secrets file set to {owner}:read.')
211-
logging.debug(f'Ownership of .secrets file given to {owner}.')
234+
os.chown(secrets_file_path, userinfo.pw_uid, userinfo.pw_gid)
235+
logging.debug(
236+
f'Permissions of {secrets_file_path} file set to {owner}:read.'
237+
)
238+
logging.debug(
239+
f'Ownership of {secrets_file_path} file given to {owner}.'
240+
)
212241
except Exception as exc:
213242
raise CEJError(
214243
f'Failed to set permissions or ownership for .secrets file.'

cmapi/mcs_cluster_tool/README.md

+47-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ $ mcs [OPTIONS] COMMAND [ARGS]...
1818
* `dbrm_backup`: Columnstore DBRM Backup.
1919
* `restore`: Restore Columnstore (and/or MariaDB) data.
2020
* `dbrm_restore`: Restore Columnstore DBRM data.
21+
* `cskeys`: Generates a random AES encryption key and init vector and writes them to disk.
22+
* `cspasswd`: Encrypt a Columnstore plaintext password...
2123
* `help-all`: Show help for all commands in man page style.
2224
* `status`: Get status information.
2325
* `stop`: Stop the Columnstore cluster.
@@ -71,7 +73,7 @@ HA S3 ( /var/lib/columnstore/storagemanager/ ) [default: no-ha]
7173
* `-q, --quiet / -no-q, --no-quiet`: Silence verbose copy command outputs. [default: no-q]
7274
* `-c, --compress TEXT`: Compress backup in X format - Options: [ pigz ].
7375
* `-P, --parallel INTEGER`: Determines if columnstore data directories will have multiple rsync running at the same time for different subfolders to parallelize writes. Ignored if "-c/--compress" argument not set. [default: 4]
74-
* `-nb, --name-backup TEXT`: Define the name of the backup - default: $(date +%m-%d-%Y) [default: 03-06-2025]
76+
* `-nb, --name-backup TEXT`: Define the name of the backup - default: $(date +%m-%d-%Y) [default: 03-12-2025]
7577
* `-r, --retention-days INTEGER`: Retain backups created within the last X days, default 0 == keep all backups. [default: 0]
7678
* `--help`: Show this message and exit.
7779

@@ -157,6 +159,50 @@ $ mcs dbrm_restore [OPTIONS]
157159
* `-ssm, --skip-storage-manager / -no-ssm, --no-skip-storage-manager`: Skip backing up storagemanager directory. [default: ssm]
158160
* `--help`: Show this message and exit.
159161

162+
## `mcs cskeys`
163+
164+
This utility generates a random AES encryption key and init vector
165+
and writes them to disk. The data is written to the file '.secrets',
166+
in the specified directory. The key and init vector are used by
167+
the utility 'cspasswd' to encrypt passwords used in Columnstore
168+
configuration files, as well as by Columnstore itself to decrypt the
169+
passwords.
170+
171+
WARNING: Re-creating the file invalidates all existing encrypted
172+
passwords in the configuration files.
173+
174+
**Usage**:
175+
176+
```console
177+
$ mcs cskeys [OPTIONS] [DIRECTORY]
178+
```
179+
180+
**Arguments**:
181+
182+
* `[DIRECTORY]`: The directory where to store the file in. [default: /var/lib/columnstore]
183+
184+
**Options**:
185+
186+
* `-u, --user TEXT`: Designate the owner of the generated file. [default: mysql]
187+
* `--help`: Show this message and exit.
188+
189+
## `mcs cspasswd`
190+
191+
Encrypt a Columnstore plaintext password using the encryption key in
192+
the key file.
193+
194+
**Usage**:
195+
196+
```console
197+
$ mcs cspasswd [OPTIONS]
198+
```
199+
200+
**Options**:
201+
202+
* `--password TEXT`: Password to encrypt/decrypt [required]
203+
* `--decrypt`: Decrypt an encrypted password instead.
204+
* `--help`: Show this message and exit.
205+
160206
## `mcs help-all`
161207

162208
Show help for all commands in man page style.

cmapi/mcs_cluster_tool/README_DEV.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@
77
```bash
88
typer mcs_cluster_tool/__main__.py utils docs --name mcs --output README.md
99
```
10+
Optionally could be generated from installed package.
11+
```bash
12+
PYTHONPATH="/usr/share/columnstore/cmapi:/usr/share/columnstore/cmapi/deps" /usr/share/columnstore/cmapi/python/bin/python3 -m typer /usr/share/columnstore/cmapi/mcs_cluster_tool/__main__.py utils docs --name mcs --output ~/README.md
13+
```
14+
- dependencies for gem build (RHEL example)
15+
```bash
16+
sudo dnf install make gcc redhat-rpm-config -y
17+
```
1018
- install `md2man` (for now it's the only one tool that make convertation without any issues)
1119
```bash
1220
sudo yum install -y ruby ruby-devel
1321
gem install md2man
1422
```
1523
- convert to perfect `.roff` file (`man` page)
1624
```bash
17-
md2man README.md > mcs.1
25+
md2man-roff README.md > mcs.1
1826
```
1927
- enjoy =)

cmapi/mcs_cluster_tool/__main__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@
3636
app.command(
3737
'dbrm_restore', rich_help_panel='Tools commands'
3838
)(restore_commands.dbrm_restore)
39-
app.command('cskeys', rich_help_panel='Tools commands')(tools_commands.cskeys)
39+
app.command(
40+
'cskeys', rich_help_panel='Tools commands',
41+
short_help=(
42+
'Generates a random AES encryption key and init vector and writes '
43+
'them to disk.'
44+
)
45+
)(tools_commands.cskeys)
4046
app.command(
4147
'cspasswd', rich_help_panel='Tools commands'
4248
)(tools_commands.cspasswd)

cmapi/mcs_cluster_tool/decorators.py

+5
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,16 @@ def wrapper(*args, **kwargs):
2525
except typer.BadParameter as err:
2626
logger.error('Bad command line parameter.')
2727
raise err
28+
except typer.Exit as err: # if some command used typer.Exit
29+
#TODO: think about universal protocol to return json data and
30+
# plain text results.
31+
return_code = err.exit_code
2832
except Exception:
2933
logger.error(
3034
'Undefined error while command execution',
3135
exc_info=True
3236
)
3337
typer.echo('Unknown error, check the log file.', err=True)
38+
3439
raise typer.Exit(return_code)
3540
return wrapper

0 commit comments

Comments
 (0)