Skip to content

Commit 5f24bfd

Browse files
authored
Merge pull request #25 from adobe-apiplatform/v2
Updates from lots of live testing edge cases. Go to RC2.
2 parents 10e4427 + b5ccb4b commit 5f24bfd

File tree

4 files changed

+79
-49
lines changed

4 files changed

+79
-49
lines changed

docs/usage-instructions-v2.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,24 +160,27 @@ as in these examples:
160160

161161
```python
162162
from umapi_client import IdentityTypes
163+
user0 = UserAction(id_type=IdentityTypes.federatedID, email="[email protected]")
163164
user1 = UserAction(id_type=IdentityTypes.adobeID, email="[email protected]")
164165
user2 = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
165166
```
166167

167-
But when Federated ID is being used, and a non-email username is being
168+
When Federated ID is being used, and a non-email username is being
168169
used to identify users across the SAML connection, both the username
169-
and the domain must be specified separately, as in these examples:
170+
and the domain must be specified, as in these examples:
170171

171172
```python
172-
user3 = UserAction(id_type=IdentityTypes.federatedID,
173+
user3 = UserAction(id_type="federatedID",
173174
username="user347", domain="division.conglomerate.com")
174-
user4 = UserAction(id_type=IdentityTypes.federatedID,
175+
user4 = UserAction(id_type="federatedID",
175176
username="user348", domain="division.conglomerate.com",
176177
177178
```
178179

180+
As these examples show, you can supply the `id_type` as a string, if desired.
181+
179182
Note that, as in the last example, it's OK to specify the email when
180-
creating a user object even if the email is not the unique ID or
183+
creating a Federated ID user object even if the email is not the unique ID or
181184
doesn't use the same domain. If
182185
you later perform an operations on a user which requires the email
183186
(such as user creation on the Adobe side), the email will be remembered

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from setuptools import setup, find_packages
2222

2323
setup(name='umapi-client',
24-
version='2.0rc1',
24+
version='2.0rc2',
2525
description='Client for the User Management API (UMAPI) from Adobe - see https://adobe.ly/2h1pHgV',
2626
long_description=('The User Management API (aka the UMAPI) is an Adobe-hosted network service '
2727
'which provides Adobe Enterprise customers the ability to manage their users. This '

tests/test_functional.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
from umapi_client import UserAction
2525

2626

27+
def test_user_emptyid():
28+
with pytest.raises(ValueError):
29+
user = UserAction(id_type=IdentityTypes.federatedID)
30+
31+
2732
def test_user_adobeid():
2833
user = UserAction(email="[email protected]")
2934
assert user.wire_dict() == {"do": [], "user": "[email protected]"}
@@ -89,8 +94,19 @@ def test_create_user_federatedid_username():
8994
"user": "dbrotsky", "domain": "k.on-the-side.net"}
9095

9196

97+
def test_create_user_federatedid_username_email():
98+
user = UserAction(id_type=IdentityTypes.federatedID,username="dbrotsky", domain="k.on-the-side.net",
99+
100+
user.create(first_name="Daniel", last_name="Brotsky", country="US")
101+
assert user.wire_dict() == {"do": [{"createFederatedID": {"email": "[email protected]",
102+
"firstName": "Daniel", "lastName": "Brotsky",
103+
"country": "US"}}],
104+
"user": "dbrotsky", "domain": "k.on-the-side.net"}
105+
106+
92107
def test_create_user_federatedid_username_mismatch():
93-
user = UserAction(id_type=IdentityTypes.federatedID, username="dbrotsky", domain="k.on-the-side.net")
108+
user = UserAction(id_type=IdentityTypes.federatedID, username="dbrotsky", domain="k.on-the-side.net",
109+
94110
with pytest.raises(ValueError):
95111
user.create(first_name="Daniel", last_name="Brotsky", country="US", email="[email protected]")
96112

@@ -130,7 +146,7 @@ def test_remove_product_federatedid():
130146

131147

132148
def test_remove_product_federatedid_all():
133-
user = UserAction(id_type=IdentityTypes.federatedID, email="[email protected]")
149+
user = UserAction(id_type='federatedID', email="[email protected]")
134150
user.remove_group(all_groups=True)
135151
assert user.wire_dict() == {"do": [{"remove": "all"}],
136152
"user": "[email protected]"}
@@ -144,7 +160,7 @@ def test_add_role_enterpriseid():
144160

145161

146162
def test_remove_role_enterpriseid():
147-
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
163+
user = UserAction(id_type='enterpriseID', email="[email protected]")
148164
user.remove_role(groups=["Photoshop", "Illustrator"])
149165
assert user.wire_dict() == {"do": [{"removeRoles": {"admin": ["Photoshop", "Illustrator"]}}],
150166
"user": "[email protected]"}
@@ -158,7 +174,7 @@ def test_remove_from_organization_federatedid():
158174

159175

160176
def test_remove_from_organization_adobeid():
161-
user = UserAction(id_type=IdentityTypes.adobeID, email="[email protected]")
177+
user = UserAction(id_type='adobeID', email="[email protected]")
162178
user.remove_from_organization()
163179
assert user.wire_dict() == {"do": [{"removeFromOrg": {}}],
164180
"user": "[email protected]"}

umapi_client/functional.py

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -51,45 +51,60 @@ class UserAction(Action):
5151
A sequence of commands to perform on a single user.
5252
"""
5353

54+
def _validate(self, email=None, username=None, domain=None):
55+
local_regex = r"[a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+([.][a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+)*"
56+
dns_regex = r"[a-zA-Z0-9-]+([.][a-zA-Z0-9-]+)+"
57+
email_regex = r"^" + local_regex + r"@" + dns_regex + r"$"
58+
username_regex = r"^" + local_regex + r"$"
59+
domain_regex = r"^" + dns_regex + r"$"
60+
if email and not re.match(email_regex, email):
61+
raise ValueError("Illegal email format (must not be quoted or contain comments)")
62+
if domain and not re.match(domain_regex, domain):
63+
raise ValueError("Illegal domain format")
64+
if username and not re.match(username_regex, username):
65+
raise ValueError("Illegal username format: must be the unquoted local part of an email")
66+
5467
def __init__(self, id_type=IdentityTypes.adobeID, email=None, username=None, domain=None, **kwargs):
5568
"""
5669
Create an Action for a user identified either by email or by username and domain.
5770
There is never a reason to specify both email and username.
71+
:param id_type: IdentityTypes enum value (or the name of one), defaults to adobeID
5872
:param username: string, username in the Adobe domain (might be email)
5973
:param domain: string, required if the username is not an email address
6074
:param kwargs: other key/value pairs for the action, such as requestID
6175
"""
76+
if str(id_type) in IdentityTypes.__members__:
77+
id_type = IdentityTypes[id_type]
6278
if id_type not in IdentityTypes:
6379
raise ValueError("Identity type ({}}) must be one of {}}".format(id_type, [i.name for i in IdentityTypes]))
80+
self.id_type = id_type
81+
self.email = None
82+
self.domain = None
83+
if username:
84+
if email and username.lower() == email.lower():
85+
# ignore the username if it's the same as the email (policy default)
86+
username = None
87+
elif id_type is not IdentityTypes.federatedID:
88+
raise ValueError("Username must match email except for Federated ID")
89+
else:
90+
self._validate(username=username)
91+
if domain:
92+
self._validate(domain=domain)
93+
self.domain = domain
6494
if email:
65-
if not re.match(r"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+([.][a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+)*"
66-
r"@"
67-
r"[a-zA-Z0-9-]+([.][a-zA-Z0-9-]+)+$", email):
68-
raise ValueError("Illegal email format (must not be quoted or contain comments)")
69-
if username and id_type is not IdentityTypes.federatedID:
70-
raise ValueError("Only in Federated ID can username be specified")
71-
self.id_type = id_type
72-
self.email = str(email)
73-
atpos = email.index('@')
74-
self.username = str(username) if username else email[0:atpos]
75-
self.domain = email[atpos + 1:]
76-
if domain and (str(domain).lower() != str(self.domain).lower()):
77-
raise ValueError("Specified domain ({}) does not match email domain ({})".format(domain, self.domain))
78-
Action.__init__(self, user=email, **kwargs)
95+
self._validate(email=email)
96+
self.email = email
97+
if not self.domain:
98+
atpos = email.index('@')
99+
self.domain = email[atpos + 1:]
100+
elif not username:
101+
raise ValueError("No user identity specified.")
102+
elif not domain:
103+
raise ValueError("Both username and domain must be specified")
104+
if username:
105+
Action.__init__(self, user=username, domain=self.domain, **kwargs)
79106
else:
80-
if not username or not domain:
81-
raise ValueError("Either email or both username and domain must be specified")
82-
if id_type is not IdentityTypes.federatedID:
83-
raise ValueError("Only in Federated ID can username be specified")
84-
if not re.match(r"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+([.][a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+)*$", username):
85-
raise ValueError("Illegal username format: must be the unquoted local part of an email")
86-
if not re.match(r"^[a-zA-Z0-9-]+([.][a-zA-Z0-9-]+)+$", domain):
87-
raise ValueError("Illegal domain format")
88-
self.id_type = id_type
89-
self.email = None
90-
self.username = str(username)
91-
self.domain = str(domain)
92-
Action.__init__(self, user=username, domain=domain, **kwargs)
107+
Action.__init__(self, user=email, **kwargs)
93108

94109
def create(self, first_name=None, last_name=None, country=None, email=None,
95110
on_conflict=IfAlreadyExistsOptions.errorIfAlreadyExists):
@@ -99,25 +114,20 @@ def create(self, first_name=None, last_name=None, country=None, email=None,
99114
:param last_name: (optional) user last name
100115
:param country: (optional except for Federated ID) user 2-letter ISO country code
101116
:param email: user email, if not already specified at create time
102-
:param on_conflict: how to handle users who already exist on the back end
117+
:param on_conflict: IfAlreadyExistsOption (or string name thereof) controlling creation of existing users
103118
:return: the User, so you can do User(...).create(...).add_group(...)
104119
"""
105120
# all types handle email and on_conflict similarly
106121
create_params = {}
107122
if email is None:
108123
email = self.email
109124
elif self.email is None:
110-
email = str(email)
111-
if not re.match(r"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+([.][a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+)*"
112-
r"@"
113-
r"[a-zA-Z0-9-]+([.][a-zA-Z0-9-]+)+$", email):
114-
raise ValueError("Illegal email format (must not be quoted or contain comments)")
125+
self._validate(email=email)
115126
self.email = email
116-
atpos = email.index('@')
117-
if self.domain.lower() != email[atpos + 1:].lower():
118-
raise ValueError("User's email ({}) doesn't match domain ({})", email, self.domain)
119-
elif self.email.lower() != str(email).lower():
127+
elif self.email.lower() != email.lower():
120128
raise ValueError("Specified email ({}) doesn't match user's email({})", email, self.email)
129+
if str(on_conflict) in IfAlreadyExistsOptions.__members__:
130+
on_conflict = IfAlreadyExistsOptions[on_conflict]
121131
if on_conflict not in IfAlreadyExistsOptions:
122132
raise ValueError("on_conflict must be one of {}".format([o.name for o in IfAlreadyExistsOptions]))
123133
if on_conflict != IfAlreadyExistsOptions.errorIfAlreadyExists:
@@ -157,8 +167,9 @@ def update(self, email=None, username=None, first_name=None, last_name=None, cou
157167
"""
158168
if self.id_type is IdentityTypes.adobeID:
159169
raise ValueError("You cannot update any attributes of an Adobe ID.")
160-
if username and self.id_type is not IdentityTypes.federatedID:
161-
raise ValueError("You can only update the username attribute of a Federated ID")
170+
if email: self._validate(email=email)
171+
if username and self.id_type is IdentityTypes.enterpriseID:
172+
self._validate(email=username)
162173
updates = {}
163174
for k, v in six.iteritems(dict(email=email, username=username,
164175
firstName=first_name, lastName=last_name,

0 commit comments

Comments
 (0)