Skip to content

Commit 1d9801b

Browse files
committed
PYCBC-1371: Implement ChangePassword
Motivation ---------- Implement change password accross SDKs Changes ------- *Added retry to connections in test to let the server process changes *Added space in user_management.hxx to fix enum *Minor test fix *Added test *rename newPassword -> new_password *Rebase *Update submodule *iSort *Pep8 *Clang-format *Import fix in users.py *Initial commit Change-Id: Ie8184c34072bed7f5441b92e4d2eb8ee39ddeb37 Reviewed-on: https://review.couchbase.org/c/couchbase-python-client/+/184806 Reviewed-by: Jared Casey <[email protected]> Tested-by: Jared Casey <[email protected]>
1 parent eb6ccef commit 1d9801b

File tree

7 files changed

+142
-6
lines changed

7 files changed

+142
-6
lines changed

couchbase/management/logic/users_logic.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
user_mgmt_operations)
3939

4040
if TYPE_CHECKING:
41-
from couchbase.management.options import (DropGroupOptions,
41+
from couchbase.management.options import (ChangePasswordOptions,
42+
DropGroupOptions,
4243
DropUserOptions,
4344
GetAllGroupsOptions,
4445
GetAllUsersOptions,
@@ -221,6 +222,38 @@ def drop_user(self,
221222

222223
return management_operation(**mgmt_kwargs)
223224

225+
def change_password(self,
226+
new_password, # type: str
227+
*options, # type: ChangePasswordOptions
228+
**kwargs # type: Any
229+
) -> None:
230+
231+
final_args = forward_args(kwargs, *options)
232+
233+
op_args = {
234+
"password": new_password
235+
}
236+
237+
mgmt_kwargs = {
238+
"conn": self._connection,
239+
"mgmt_op": mgmt_operations.USER.value,
240+
"op_type": user_mgmt_operations.CHANGE_PASSWORD.value,
241+
"op_args": op_args
242+
}
243+
244+
callback = kwargs.pop('callback', None)
245+
if callback:
246+
mgmt_kwargs['callback'] = callback
247+
248+
errback = kwargs.pop('errback', None)
249+
if errback:
250+
mgmt_kwargs['errback'] = errback
251+
252+
if final_args.get("timeout", None) is not None:
253+
mgmt_kwargs["timeout"] = final_args.get("timeout")
254+
255+
return management_operation(**mgmt_kwargs)
256+
224257
def get_roles(self,
225258
*options, # type: GetRolesOptions
226259
**kwargs # type: Any

couchbase/management/options.py

+15
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,21 @@ class UpsertUserOptions(UserOptions):
330330
"""
331331

332332

333+
class ChangePasswordOptions(UserOptions):
334+
"""Available options to for a :class:`~couchbase.management.users.UserManager`'s change password user
335+
operation.
336+
337+
.. note::
338+
All management options should be imported from ``couchbase.management.options``.
339+
340+
Args:
341+
domain_name (str, optional): The user's domain name (either ``local`` or ``external``). Defaults
342+
to ``local``.
343+
timeout (timedelta, optional): The timeout for this operation. Defaults to global
344+
management operation timeout.
345+
"""
346+
347+
333348
class DropUserOptions(UserOptions):
334349
"""Available options to for a :class:`~couchbase.management.users.UserManager`'s drop user
335350
operation.

couchbase/management/users.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
from couchbase.management.logic.wrappers import BlockingMgmtWrapper, ManagementType
2727

2828
# @TODO: lets deprecate import of options from couchbase.management.users
29-
from couchbase.management.options import (DropGroupOptions,
29+
from couchbase.management.options import (ChangePasswordOptions,
30+
DropGroupOptions,
3031
DropUserOptions,
3132
GetAllGroupsOptions,
3233
GetAllUsersOptions,
@@ -48,7 +49,7 @@ def get_user(self,
4849
*options, # type: GetUserOptions
4950
**kwargs # type: Any
5051
) -> UserAndMetadata:
51-
"""Returns a user by it's username.
52+
"""Returns a user by its username.
5253
5354
Args:
5455
username (str): The name of the user to retrieve.
@@ -124,6 +125,29 @@ def drop_user(self,
124125
"""
125126
return super().drop_user(username, *options, **kwargs)
126127

128+
@BlockingMgmtWrapper.block(None, ManagementType.UserMgmt, UserManagerLogic._ERROR_MAPPING)
129+
def change_password(self,
130+
new_password, # type: str
131+
*options, # type: ChangePasswordOptions
132+
**kwargs # type: Any
133+
) -> None:
134+
"""Changes the password of the currently authenticated user. SDK must be re-started and a new connection
135+
established after running, as the previous credentials will no longer be valid.
136+
137+
Args:
138+
new_password (str): The new password for the user
139+
options (:class:`~couchbase.management.options.ChangePasswordOptions`): Optional parameters for this
140+
operation.
141+
**kwargs (Dict[str, Any]): keyword arguments that can be used as optional parameters
142+
for this operation.
143+
144+
Raises:
145+
:class:`~couchbase.exceptions.InvalidArgumentException`: If the provided group argument contains an
146+
invalid value or type.
147+
148+
"""
149+
return super().change_password(new_password, *options, **kwargs)
150+
127151
@BlockingMgmtWrapper.block(RoleAndDescription, ManagementType.UserMgmt, UserManagerLogic._ERROR_MAPPING)
128152
def get_roles(self,
129153
*options, # type: GetRolesOptions

couchbase/tests/usermgmt_t.py

+51-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
# limitations under the License.
1515

1616
import re
17+
from datetime import timedelta
1718

1819
import pytest
1920

20-
from couchbase.exceptions import (CouchbaseException,
21+
from couchbase.exceptions import (AuthenticationException,
22+
CouchbaseException,
2123
FeatureUnavailableException,
2224
GroupNotFoundException,
2325
InvalidArgumentException,
@@ -29,7 +31,11 @@
2931
from couchbase.management.users import (Group,
3032
Role,
3133
User)
34+
from couchbase.options import ClusterOptions
35+
from tests.helpers import ClusterInformation
3236

37+
from ..auth import PasswordAuthenticator
38+
from ..cluster import Cluster
3339
from ._test_utils import TestEnvironment
3440

3541

@@ -223,6 +229,50 @@ def test_user_display_name(self, cb_env):
223229

224230
cb_env.um.drop_user(user.username, DropUserOptions(domain_name="local"))
225231

232+
def test_user_change_password(self, cb_env):
233+
username = 'change-password-user'
234+
admin_role = Role(name='admin')
235+
original_password = 'original_password'
236+
new_password = 'new_password'
237+
238+
change_password_user = User(username=username,
239+
display_name="Change Password User",
240+
roles=admin_role,
241+
password=original_password)
242+
# Upsert user
243+
cb_env.um.upsert_user(change_password_user, UpsertUserOptions(domain_name="local"))
244+
245+
# Authenticate as change-password-user.
246+
# Done in a while loop to emulate retry
247+
auth = PasswordAuthenticator(username, original_password)
248+
new_cluster = None
249+
while True:
250+
try:
251+
new_cluster = Cluster.connect(cb_env.cluster._connstr, ClusterOptions(auth))
252+
except AuthenticationException:
253+
continue
254+
break
255+
256+
# Change password
257+
new_cluster.users().change_password(new_password)
258+
259+
# Assert can authenticate using new password
260+
success_auth = PasswordAuthenticator(username, new_password)
261+
while True:
262+
try:
263+
success_cluster = Cluster.connect(cb_env.cluster._connstr, ClusterOptions(success_auth))
264+
except AuthenticationException:
265+
continue
266+
success_cluster.close()
267+
break
268+
269+
# Assert cannot authenticate using old password
270+
fail_auth = PasswordAuthenticator(username, original_password)
271+
with pytest.raises(AuthenticationException):
272+
Cluster.connect(cb_env.cluster._connstr, ClusterOptions(fail_auth))
273+
274+
new_cluster.close()
275+
226276
def test_external_user(self, cb_env):
227277
"""
228278
test_external_user()

src/management/user_management.cxx

+12
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,18 @@ handle_user_mgmt_op(connection* conn, struct user_mgmt_options* options, PyObjec
905905
*conn, req, pyObj_callback, pyObj_errback, barrier);
906906
break;
907907
}
908+
case UserManagementOperations::CHANGE_PASSWORD: {
909+
PyObject* pyObj_newPassword = PyDict_GetItemString(options->op_args, "password");
910+
auto newPassword = std::string(PyUnicode_AsUTF8(pyObj_newPassword));
911+
912+
couchbase::core::operations::management::change_password_request req{};
913+
req.newPassword = newPassword;
914+
req.timeout = options->timeout_ms;
915+
916+
res = do_user_mgmt_op<couchbase::core::operations::management::change_password_request>(
917+
*conn, req, pyObj_callback, pyObj_errback, barrier);
918+
break;
919+
}
908920
case UserManagementOperations::GET_ROLES: {
909921
couchbase::core::operations::management::role_get_all_request req{};
910922
req.timeout = options->timeout_ms;

src/management/user_management.hxx

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class UserManagementOperations
2929
GET_USER,
3030
GET_ALL_USERS,
3131
DROP_USER,
32+
CHANGE_PASSWORD,
3233
GET_ROLES,
3334
UPSERT_GROUP,
3435
GET_GROUP,
@@ -66,11 +67,12 @@ class UserManagementOperations
6667
"GET_USER "
6768
"GET_ALL_USERS "
6869
"DROP_USER "
70+
"CHANGE_PASSWORD "
6971
"GET_ROLES "
7072
"UPSERT_GROUP "
7173
"GET_GROUP "
7274
"GET_ALL_GROUPS "
73-
"DROP_GROUP";
75+
"DROP_GROUP ";
7476

7577
return ops;
7678
}

0 commit comments

Comments
 (0)