Skip to content

Commit 8294eb5

Browse files
committed
Merge branch 'feature/credential-manager' into feature/multi-directory-cred
2 parents a9d647d + 03c3f08 commit 8294eb5

File tree

11 files changed

+346
-105
lines changed

11 files changed

+346
-105
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ endif
1515

1616
standalone:
1717
python -m pip install --upgrade pip
18-
python -m pip install --upgrade pyinstaller
18+
python -m pip install --upgrade pyinstaller==4.0
1919
python -m pip install --upgrade setuptools
2020
-$(RM) $(output_dir)
2121
python .build/pre_build.py

docs/en/user-manual/additional_tools.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ in OS Secure storage.
104104
Successful console output after running the ```store``` command:
105105

106106
```
107+
To store the priv_key_data when it's too long
108+
In order to save the priv_key_data which is present in the connector-umapi or connector-adobe-console
109+
file, Credential Store command will attempt to store the key data in the OS.
110+
Window credential store generally can't store data as large as Private Key. To store the key, Sync Tool
111+
asks the user to encrypt the key.
112+
If yes then user will be prompted for a password which will be saved as priv_key_pass and the encrypted key itself will be saved as priv_key_data in the file.
113+
If no, the key data will remain in the file in an unencrypted state.
114+
115+
Refer to the (URL)
116+
107117
(venv) C:\Program Files\Adobe\Adobe User Sync Tool>python user-sync.pex credentials store
108118
<output from store goes here>
109119
```

examples/config files - basic/connector-adobe-console.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ integration:
4444
# data directly in this file. To do this, remove the priv_key_path entry above
4545
# and uncomment the following entry. Replace the sample data with the data
4646
# from your private key file (which will be much longer).
47-
#priv_key_data: |
47+
# priv_key_data: |
4848
# -----BEGIN RSA PRIVATE KEY-----
4949
# MIIf74jfd84oAgEA6brj4uZ2f1Nkf84j843jfjjJGHYJ8756GHHGGz7jLyZWSscH
5050
# CoifurKJY763GHKL98mJGYxWSBvhlWskdjdatagoeshere986fKFUNGd74kdfuEH
@@ -70,7 +70,6 @@ integration:
7070
# Having done this, you can use the setting priv_key_pass to specify the passphrase needed
7171
# by User Sync to decrypt the private key file (or private key data), as in:
7272
# priv_key_pass: "my passphrase for my private key"
73-
7473
# Private key pass can also be stored using credentials store command
7574

7675
# (optional) identity_type_filter (default value is all)

examples/config files - basic/connector-umapi.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,33 @@ server:
4343
# the correct settings for your enterprise organization.
4444
# [NOTE: the priv_key_path setting can be an absolute or relative pathname;
4545
# if relative, it is interpreted relative to this configuration file.]
46+
47+
# (optional) You can store credentials in the operating system credential store
48+
# (Windows Credential Manager, Mac Keychain, Linux Freedesktop Secret Service
49+
# or KWallet - these will be built into the Linux distribution).
50+
# To use this feature, You can secure the credentials by using the credentials store command in the command line
51+
# Refer to the (URL to Additional Tools)
52+
53+
# (optional) You can encrypt the private key by running `user-sync.exe encrypt'.
54+
# This will encrypt the private key as RSA 256. you should uncomment
55+
# priv_key_pass key so that User Sync is knows to decrypt the key. priv_key_pass should have the plaintext value
56+
# of the password (or, see credential storage above). The decryption also works for keys encrypted
57+
# externally with OpenSSL or other libraries, provided the format is correct.
58+
4659
enterprise:
4760
org_id: "Org ID goes here"
4861
client_id: "Client ID goes here"
4962
client_secret: "Client secret goes here"
5063
tech_acct_id: "Tech account ID goes here"
5164
priv_key_path: "private.key"
65+
#priv_key_pass: "my passphrase for my private key"
5266

5367
# (optional) As an alternative to priv_key_path, you can place the private key
5468
# data directly in this file. To do this, remove the priv_key_path entry above
5569
# and uncomment the following entry. Replace the sample data with the data
5670
# from your private key file (which will be much longer).
71+
# Note: the Windows credential store can't store data as large as a private
72+
# key, so Windows users will be prompted to encrypt the private key data instead.
5773
#priv_key_data: |
5874
# -----BEGIN RSA PRIVATE KEY-----
5975
# MIIf74jfd84oAgEA6brj4uZ2f1Nkf84j843jfjjJGHYJ8756GHHGGz7jLyZWSscH

tests/test_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ def test_get_credential_old_format(tmp_config_files):
302302
with pytest.raises(AssertionException):
303303
ldap_dict_config.get_credential('password', 'user_sync')
304304
username = ldap_config['username']
305-
credman.set('ldap_secure_identifier', 'test_password', username)
305+
credman.set('ldap_secure_identifier', 'test_password', username=username)
306306
# set the plain key to None so get_credential will look for the secure_password_key format
307307
ldap_config['password'] = None
308308
assert ldap_dict_config.get_credential('password', username) == 'test_password'

tests/test_credentials.py

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import os
22
import uuid
33

4+
import keyring
45
import pytest
6+
import yaml
57

6-
from user_sync.credentials import *
7-
from user_sync.error import AssertionException
8+
from tests.util import update_dict
9+
from user_sync import encryption
10+
from user_sync.credentials import CredentialConfig, CredentialManager, Key, LdapCredentialConfig, UmapiCredentialConfig
811

912

1013
@pytest.fixture
@@ -17,6 +20,19 @@ def ldap_config_file(fixture_dir):
1720
return os.path.join(fixture_dir, 'connector-ldap.yml')
1821

1922

23+
@pytest.fixture
24+
def modify_umapi_config(tmp_config_files):
25+
(_, _, umapi_config_file) = tmp_config_files
26+
27+
def _modify_umapi_config(keys, val):
28+
conf = yaml.safe_load(open(umapi_config_file))
29+
conf = update_dict(conf, keys, val)
30+
yaml.dump(conf, open(umapi_config_file, 'w'))
31+
32+
return umapi_config_file
33+
return _modify_umapi_config
34+
35+
2036
def test_nested_set(ldap_config_file):
2137
c = CredentialConfig(ldap_config_file)
2238
c.set_nested_key(['password'], {'secure': 'somethingverysecure'})
@@ -27,37 +43,37 @@ def test_nested_set(ldap_config_file):
2743
def test_retrieve_ldap_creds_valid(tmp_config_files):
2844
(_, ldap_config_file, _) = tmp_config_files
2945
c = CredentialConfig(ldap_config_file)
30-
key_list = ['password']
31-
plaintext_cred = c.get_nested_key(key_list)
32-
c.store_key(key_list)
33-
retrieved_plaintext_cred = c.retrieve_key(key_list)
46+
key = Key(['password'])
47+
plaintext_cred = c.get_nested_key(key.key_path)
48+
c.store_key(key)
49+
retrieved_plaintext_cred = c.retrieve_key(key)
3450
assert retrieved_plaintext_cred == plaintext_cred
3551

3652

3753
def test_retrieve_ldap_creds_invalid(tmp_config_files):
3854
(_, ldap_config_file, _) = tmp_config_files
3955
c = CredentialConfig(ldap_config_file)
40-
key_list = ['password']
56+
key = Key(['password'])
4157
# if store_key has not been called previously, retrieve_key returns None
42-
assert c.retrieve_key(key_list) is None
58+
assert c.retrieve_key(key) is None
4359

4460

4561
def test_revert_valid(tmp_config_files):
4662
(_, ldap_config_file, _) = tmp_config_files
4763
c = CredentialConfig(ldap_config_file)
48-
key_list = ['password']
49-
plaintext_cred = c.get_nested_key(key_list)
50-
c.store_key(key_list)
51-
reverted_plaintext_cred = c.revert_key(key_list)
64+
key = Key(['password'])
65+
plaintext_cred = c.get_nested_key(key.key_path)
66+
c.store_key(key)
67+
reverted_plaintext_cred = c.revert_key(key)
5268
assert reverted_plaintext_cred == plaintext_cred
5369

5470

5571
def test_revert_invalid(tmp_config_files):
5672
(_, ldap_config_file, _) = tmp_config_files
5773
c = CredentialConfig(ldap_config_file)
58-
key_list = ['password']
74+
key = Key(['password'])
5975
# assume store_key has not been called
60-
assert c.revert_key(key_list) is None
76+
assert c.revert_key(key) is None
6177

6278

6379
def test_retrieve_revert_ldap_valid(tmp_config_files):
@@ -84,20 +100,18 @@ def test_retrieve_revert_ldap_invalid(tmp_config_files):
84100
# if store has not been previously called before retrieve and revert we can expect the following
85101
retrieved_key_dict = ldap.retrieve()
86102
assert retrieved_key_dict == {}
87-
88103
creds = ldap.revert()
89104
assert creds == {}
90105

91106

92-
93-
def test_retrieve_revert_umapi_valid(tmp_config_files):
94-
(_, _, umapi_config_file) = tmp_config_files
95-
umapi = UmapiCredentialConfig(umapi_config_file)
107+
def test_retrieve_revert_umapi_valid(private_key, modify_umapi_config):
108+
umapi_config_file = modify_umapi_config(['enterprise', 'priv_key_path'], private_key)
109+
umapi = UmapiCredentialConfig(umapi_config_file, auto=True)
96110
# Using the api_key for assertions. The rest can be added in later if deemed necessary
97111
assert not umapi.parse_secure_key(umapi.get_nested_key(['enterprise', 'client_id']))
98112
unsecured_api_key = umapi.get_nested_key(['enterprise', 'client_id'])
99113
umapi.store()
100-
with open(umapi_config_file) as f:
114+
with open(umapi_config_file, 'r') as f:
101115
data = yaml.load(f)
102116
assert umapi.parse_secure_key(data['enterprise']['client_id'])
103117
retrieved_key_dict = umapi.retrieve()
@@ -108,9 +122,10 @@ def test_retrieve_revert_umapi_valid(tmp_config_files):
108122
assert data['enterprise']['client_id'] == unsecured_api_key
109123

110124

111-
def test_credman_retrieve_revert_valid(tmp_config_files):
112-
(root_config_file, ldap_config_file, umapi_config_file) = tmp_config_files
113-
credman = CredentialManager(root_config_file)
125+
def test_credman_retrieve_revert_valid(tmp_config_files, private_key, modify_umapi_config):
126+
(root_config_file, ldap_config_file, _) = tmp_config_files
127+
umapi_config_file = modify_umapi_config(['enterprise', 'priv_key_path'], private_key)
128+
credman = CredentialManager(root_config_file, auto=True)
114129
with open(ldap_config_file) as f:
115130
data = yaml.load(f)
116131
plaintext_ldap_password = data['password']
@@ -137,8 +152,9 @@ def test_credman_retrieve_revert_valid(tmp_config_files):
137152
assert data['enterprise']['client_id'] == plaintext_umapi_api_key
138153

139154

140-
def test_credman_retrieve_revert_invalid(tmp_config_files):
141-
(root_config_file, ldap_config_file, umapi_config_file) = tmp_config_files
155+
def test_credman_retrieve_revert_invalid(tmp_config_files, private_key, modify_umapi_config):
156+
(root_config_file, ldap_config_file, _) = tmp_config_files
157+
umapi_config_file = modify_umapi_config(['enterprise', 'priv_key_path'], private_key)
142158
credman = CredentialManager(root_config_file)
143159
# if credman.store() has not been called first then we can expect the following
144160
retrieved_creds = credman.retrieve()
@@ -147,7 +163,6 @@ def test_credman_retrieve_revert_invalid(tmp_config_files):
147163
assert creds == {}
148164

149165

150-
151166
def test_set():
152167
identifier = 'TestId'
153168
value = 'TestValue'
@@ -170,7 +185,7 @@ def test_set_long():
170185
value = "".join([str(uuid.uuid4()) for x in range(500)])
171186

172187
if isinstance(keyring.get_keyring(), keyring.backends.Windows.WinVaultKeyring):
173-
with pytest.raises(AssertionException):
188+
with pytest.raises(Exception):
174189
cm.set(identifier, value)
175190
else:
176191
cm.set(identifier, value)
@@ -189,7 +204,8 @@ def test_get_not_valid():
189204
def test_config_store(tmp_config_files):
190205
(_, ldap_config_file, _) = tmp_config_files
191206
ldap = LdapCredentialConfig(ldap_config_file)
192-
assert not ldap.parse_secure_key(ldap.get_nested_key(['password']))
207+
key = Key(['password'])
208+
assert not ldap.parse_secure_key(ldap.get_nested_key(key.key_path))
193209
ldap.store()
194210
with open(ldap_config_file) as f:
195211
data = yaml.load(f)
@@ -199,14 +215,52 @@ def test_config_store(tmp_config_files):
199215
def test_config_store_key(tmp_config_files):
200216
(_, ldap_config_file, _) = tmp_config_files
201217
ldap = LdapCredentialConfig(ldap_config_file)
202-
assert not ldap.parse_secure_key(ldap.get_nested_key(['password']))
203-
ldap.store_key(['password'])
204-
assert ldap.parse_secure_key(ldap.get_nested_key(['password']))
218+
key = Key(['password'])
219+
assert not ldap.parse_secure_key(ldap.get_nested_key(key.key_path))
220+
ldap.store_key(key)
221+
assert ldap.parse_secure_key(ldap.get_nested_key(key.key_path))
205222

206223

207224
def test_config_store_key_none(tmp_config_files):
208225
(_, ldap_config_file, _) = tmp_config_files
209226
ldap = LdapCredentialConfig(ldap_config_file)
210-
ldap.set_nested_key(['password'], [])
211-
with pytest.raises(AssertionException):
212-
ldap.store_key(['password'])
227+
key = Key(['password'])
228+
ldap.set_nested_key(key.key_path, [])
229+
assert ldap.store_key(key) is None
230+
231+
232+
def test_credman_encrypt_decrypt_key_path(tmp_config_files, private_key, modify_umapi_config):
233+
(root_config_file, ldap_config_file, _) = tmp_config_files
234+
umapi_config_file = modify_umapi_config(['enterprise', 'priv_key_path'], private_key)
235+
credman = CredentialManager(root_config_file, auto=True)
236+
with open(private_key) as f:
237+
key_data = f.read()
238+
assert encryption.is_encryptable(key_data)
239+
credman.store()
240+
with open(private_key) as f:
241+
key_data = f.read()
242+
assert not encryption.is_encryptable(key_data)
243+
credman.revert()
244+
with open(private_key) as f:
245+
key_data = f.read()
246+
assert encryption.is_encryptable(key_data)
247+
248+
249+
def test_credman_encrypt_decrypt_key_data(tmp_config_files, private_key, modify_umapi_config):
250+
(root_config_file, ldap_config_file, _) = tmp_config_files
251+
umapi_config_file = modify_umapi_config(['enterprise', 'priv_key_path'], None)
252+
with open(private_key) as f:
253+
key_data = f.read()
254+
umapi_config_file = modify_umapi_config(['enterprise', 'priv_key_data'], key_data)
255+
credman = CredentialManager(root_config_file, auto=True)
256+
with open(umapi_config_file) as f:
257+
umapi_dict = yaml.load(f)
258+
assert encryption.is_encryptable(umapi_dict['enterprise']['priv_key_data'])
259+
credman.store()
260+
with open(umapi_config_file) as f:
261+
umapi_dict = yaml.load(f)
262+
assert not encryption.is_encryptable(umapi_dict['enterprise']['priv_key_data'])
263+
credman.revert()
264+
with open(umapi_config_file) as f:
265+
umapi_dict = yaml.load(f)
266+
assert encryption.is_encryptable(umapi_dict['enterprise']['priv_key_data'])

tests/test_umapi_util.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,15 @@ def test_make_auth_dict(umapi_config_file, private_key):
4141
invalid_auth_dict = make_auth_dict(name, umapi_dict_config, org_id_from_file, tech_acct_from_file, logger)
4242
# and if there is only the secure format it should work as long as the credential has been set
4343
credman = CredentialManager()
44-
credman.set('make_auth_identifier', 'keydata', org_id_from_file)
44+
credman.set('make_auth_identifier', 'keydata', username=org_id_from_file)
4545
umapi_config['enterprise']['priv_key_data'] = None
4646
auth_dict = make_auth_dict(name, umapi_dict_config, org_id_from_file, tech_acct_from_file, logger)
4747
assert auth_dict['private_key_data'] == 'keydata'
48+
# clear out old format (already tested for the exception if both are provided)
49+
umapi_config['enterprise']['secure_priv_key_data_key'] = None
50+
# put the regular priv_key_data back in the file and store
51+
# the first 256 chars so it'll work without cryptfile on Windows
52+
umapi_config['enterprise']['priv_key_data'] = {'secure': 'truncated_key_data'}
53+
credman.set('truncated_key_data', key_data_from_file[:255])
54+
auth_dict = make_auth_dict(name, umapi_dict_config, org_id_from_file, tech_acct_from_file, logger)
55+
assert auth_dict['private_key_data'] == key_data_from_file[:255]

0 commit comments

Comments
 (0)