Skip to content

Commit 734fa02

Browse files
d-w-mooretrel
authored andcommitted
[#328] allow user to change own password
1 parent 621d38c commit 734fa02

6 files changed

Lines changed: 225 additions & 14 deletions

File tree

irods/manager/user_manager.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import absolute_import
22
import logging
3+
import os
34

45
from irods.models import User, UserGroup
56
from irods.manager import Manager
6-
from irods.message import GeneralAdminRequest, iRODSMessage, GetTempPasswordForOtherRequest, GetTempPasswordForOtherOut
7+
from irods.message import UserAdminRequest, GeneralAdminRequest, iRODSMessage, GetTempPasswordForOtherRequest, GetTempPasswordForOtherOut
78
from irods.exception import UserDoesNotExist, UserGroupDoesNotExist, NoResultFound, CAT_SQL_ERR
89
from irods.api_number import api_number
910
from irods.user import iRODSUser, iRODSUserGroup
@@ -82,6 +83,70 @@ def temp_password_for_user(self, user_name):
8283
return obf.create_temp_password(msg.stringToHashWith, conn.account.password)
8384

8485

86+
class EnvStoredPasswordNotEdited(RuntimeError):
87+
88+
"""
89+
Error thrown by a password change attempt if a login password encoded in the
90+
irods environment could not be updated.
91+
92+
This error will be seen when `modify_irods_authentication_file' is set True and the
93+
authentication scheme in effect for the session is other than iRODS native,
94+
using a password loaded from the client environment.
95+
"""
96+
97+
pass
98+
99+
@staticmethod
100+
def abspath_exists(path):
101+
return (isinstance(path,str) and
102+
os.path.isabs(path) and
103+
os.path.exists(path))
104+
105+
def modify_password(self, old_value, new_value, modify_irods_authentication_file = False):
106+
107+
"""
108+
Change the password for the current user (in the manner of `ipasswd').
109+
110+
Parameters:
111+
old_value - the currently valid (old) password
112+
new_value - the desired (new) password
113+
modify_irods_authentication_file - Can be False, True, or a string. If a string, it should indicate
114+
the absolute path of an IRODS_AUTHENTICATION_FILE to be altered.
115+
"""
116+
with self.sess.pool.get_connection() as conn:
117+
118+
hash_new_value = obf.obfuscate_new_password(new_value, old_value, conn.client_signature)
119+
120+
message_body = UserAdminRequest(
121+
"userpw",
122+
self.sess.username,
123+
"password",
124+
hash_new_value
125+
)
126+
request = iRODSMessage("RODS_API_REQ", msg=message_body,
127+
int_info=api_number['USER_ADMIN_AN'])
128+
129+
conn.send(request)
130+
response = conn.recv()
131+
if modify_irods_authentication_file:
132+
auth_file = self.sess.auth_file
133+
if not auth_file or isinstance(modify_irods_authentication_file, str):
134+
auth_file = (modify_irods_authentication_file if self.abspath_exists(modify_irods_authentication_file) else '')
135+
if not auth_file:
136+
message = "Session not loaded from an environment file."
137+
raise UserManager.EnvStoredPasswordNotEdited(message)
138+
else:
139+
with open(auth_file) as f:
140+
stored_pw = obf.decode(f.read())
141+
if stored_pw != old_value:
142+
message = "Not changing contents of '{}' - "\
143+
"stored password is non-native or false match to old password".format(auth_file)
144+
raise UserManager.EnvStoredPasswordNotEdited(message)
145+
with open(auth_file,'w') as f:
146+
f.write(obf.encode(new_value))
147+
148+
logger.debug(response.int_info)
149+
85150
def modify(self, user_name, option, new_value, user_zone=""):
86151

87152
# must append zone to username for this API call

irods/message/__init__.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -685,14 +685,14 @@ class VersionResponse(Message):
685685
cookie = IntegerProperty()
686686

687687

688-
# define generalAdminInp_PI "str *arg0; str *arg1; str *arg2; str *arg3;
689-
# str *arg4; str *arg5; str *arg6; str *arg7; str *arg8; str *arg9;"
688+
class _admin_request_base(Message):
690689

691-
class GeneralAdminRequest(Message):
692-
_name = 'generalAdminInp_PI'
690+
_name = None
693691

694692
def __init__(self, *args):
695-
super(GeneralAdminRequest, self).__init__()
693+
if self.__class__._name is None:
694+
raise NotImplementedError
695+
super(_admin_request_base, self).__init__()
696696
for i in range(10):
697697
if i < len(args) and args[i]:
698698
setattr(self, 'arg{0}'.format(i), args[i])
@@ -711,6 +711,20 @@ def __init__(self, *args):
711711
arg9 = StringProperty()
712712

713713

714+
# define generalAdminInp_PI "str *arg0; str *arg1; str *arg2; str *arg3;
715+
# str *arg4; str *arg5; str *arg6; str *arg7; str *arg8; str *arg9;"
716+
717+
class GeneralAdminRequest(_admin_request_base):
718+
_name = 'generalAdminInp_PI'
719+
720+
721+
# define userAdminInp_PI "str *arg0; str *arg1; str *arg2; str *arg3;
722+
# str *arg4; str *arg5; str *arg6; str *arg7; str *arg8; str *arg9;"
723+
724+
class UserAdminRequest(_admin_request_base):
725+
_name = 'userAdminInp_PI'
726+
727+
714728
class GetTempPasswordForOtherRequest(Message):
715729
_name = 'getTempPasswordForOtherInp_PI'
716730
targetUser = StringProperty()

irods/session.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,19 @@
2121

2222
class iRODSSession(object):
2323

24+
@property
25+
def env_file (self):
26+
return self._env_file
27+
28+
@property
29+
def auth_file (self):
30+
return self._auth_file
31+
2432
def __init__(self, configure=True, **kwargs):
2533
self.pool = None
2634
self.numThreads = 0
27-
35+
self._env_file = ''
36+
self._auth_file = ''
2837
self.do_configure = (kwargs if configure else {})
2938
self.__configured = None
3039
if configure:
@@ -77,7 +86,7 @@ def _configure_account(self, **kwargs):
7786
return iRODSAccount(**kwargs)
7887

7988
# Get credentials from irods environment file
80-
creds = self.get_irods_env(env_file)
89+
creds = self.get_irods_env(env_file, session_ = self)
8190

8291
# Update with new keywords arguments only
8392
creds.update((key, value) for key, value in kwargs.items() if key not in creds)
@@ -104,7 +113,7 @@ def _configure_account(self, **kwargs):
104113
except KeyError:
105114
pass
106115

107-
creds['password'] = self.get_irods_password(**creds)
116+
creds['password'] = self.get_irods_password(session_ = self, **creds)
108117

109118
return iRODSAccount(**creds)
110119

@@ -180,16 +189,19 @@ def get_irods_password_file():
180189
return os.path.expanduser('~/.irods/.irodsA')
181190

182191
@staticmethod
183-
def get_irods_env(env_file):
192+
def get_irods_env(env_file, session_ = None):
184193
try:
185194
with open(env_file, 'rt') as f:
186-
return json.load(f)
195+
j = json.load(f)
196+
if session_ is not None:
197+
session_._env_file = env_file
198+
return j
187199
except IOError:
188200
logger.debug("Could not open file {}".format(env_file))
189201
return {}
190202

191203
@staticmethod
192-
def get_irods_password(**kwargs):
204+
def get_irods_password(session_ = None, **kwargs):
193205
try:
194206
irods_auth_file = kwargs['irods_authentication_file']
195207
except KeyError:
@@ -200,12 +212,18 @@ def get_irods_password(**kwargs):
200212
except KeyError:
201213
uid = None
202214

215+
_retval = ''
216+
203217
try:
204218
with open(irods_auth_file, 'r') as f:
205-
return decode(f.read().rstrip('\n'), uid)
219+
_retval = decode(f.read().rstrip('\n'), uid)
220+
return _retval
206221
except IOError as exc:
207222
if exc.errno != errno.ENOENT: raise # Auth file exists but can't be read
208223
return '' # No auth file (as with anonymous user)
224+
finally:
225+
if session_ is not None and _retval:
226+
session_._auth_file = irods_auth_file
209227

210228
def get_connection_refresh_time(self, **kwargs):
211229
connection_refresh_time = -1

irods/test/helpers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import threading
1313
import random
1414
import datetime
15+
import json
1516
from pwd import getpwnam
1617
from irods.session import iRODSSession
1718
from irods.message import iRODSMessage
19+
from irods.password_obfuscation import encode
1820
from six.moves import range
1921

2022

@@ -72,6 +74,20 @@ def get_register_resource(session):
7274
return Reg_Resc_Name
7375

7476

77+
def make_environment_and_auth_files( dir_, **params ):
78+
if not os.path.exists(dir_): os.mkdir(dir_)
79+
def recast(k):
80+
return 'irods_' + k + ('_name' if k in ('user','zone') else '')
81+
config = os.path.join(dir_,'irods_environment.json')
82+
with open(config,'w') as f1:
83+
json.dump({recast(k):v for k,v in params.items() if k != 'password'},f1,indent=4)
84+
auth = os.path.join(dir_,'.irodsA')
85+
with open(auth,'w') as f2:
86+
f2.write(encode(params['password']))
87+
os.chmod(auth,0o600)
88+
return (config, auth)
89+
90+
7591
def make_session(**kwargs):
7692
try:
7793
env_file = kwargs.pop('irods_env_file')

irods/test/user_group_test.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
import os
44
import sys
55
import unittest
6-
from irods.exception import UserGroupDoesNotExist
6+
import tempfile
7+
import shutil
8+
from irods.exception import UserGroupDoesNotExist, UserDoesNotExist
79
from irods.meta import iRODSMetaCollection, iRODSMeta
810
from irods.models import User, UserGroup, UserMeta
11+
from irods.session import iRODSSession
12+
import irods.exception as ex
913
import irods.test.helpers as helpers
1014
from six.moves import range
1115

@@ -20,6 +24,95 @@ def tearDown(self):
2024
'''
2125
self.sess.cleanup()
2226

27+
def test_modify_password__328(self):
28+
ses = self.sess
29+
if ses.users.get( ses.username ).type != 'rodsadmin':
30+
self.skipTest( 'Only a rodsadmin may run this test.')
31+
32+
OLDPASS = 'apass'
33+
NEWPASS = 'newpass'
34+
try:
35+
ses.users.create('alice', 'rodsuser')
36+
ses.users.modify('alice', 'password', OLDPASS)
37+
38+
with iRODSSession(user='alice', password=OLDPASS, host=ses.host, port=ses.port, zone=ses.zone) as alice:
39+
me = alice.users.get(alice.username)
40+
me.modify_password(OLDPASS, NEWPASS)
41+
42+
with iRODSSession(user='alice', password=NEWPASS, host=ses.host, port=ses.port, zone=ses.zone) as alice:
43+
home = helpers.home_collection( alice )
44+
alice.collections.get( home ) # Non-trivial operation to test success!
45+
finally:
46+
try:
47+
ses.users.get('alice').remove()
48+
except UserDoesNotExist:
49+
pass
50+
51+
@staticmethod
52+
def do_something(session):
53+
return session.username in [i[User.name] for i in session.query(User)]
54+
55+
def test_modify_password_with_changing_auth_file__328(self):
56+
ses = self.sess
57+
if ses.users.get( ses.username ).type != 'rodsadmin':
58+
self.skipTest( 'Only a rodsadmin may run this test.')
59+
OLDPASS = 'apass'
60+
def generator(p = OLDPASS):
61+
n = 1
62+
old_pw = p
63+
while True:
64+
pw = p + str(n)
65+
yield old_pw, pw
66+
n += 1; old_pw = pw
67+
password_generator = generator()
68+
ENV_DIR = tempfile.mkdtemp()
69+
d = dict(password = OLDPASS, user = 'alice', host = ses.host, port = ses.port, zone = ses.zone)
70+
(alice_env, alice_auth) = helpers.make_environment_and_auth_files(ENV_DIR, **d)
71+
try:
72+
ses.users.create('alice', 'rodsuser')
73+
ses.users.modify('alice', 'password', OLDPASS)
74+
for modify_option, sess_factory in [ (alice_auth, lambda: iRODSSession(**d)),
75+
(True,
76+
lambda: helpers.make_session(irods_env_file = alice_env,
77+
irods_authentication_file = alice_auth)) ]:
78+
OLDPASS,NEWPASS=next(password_generator)
79+
with sess_factory() as alice_ses:
80+
alice = alice_ses.users.get(alice_ses.username)
81+
alice.modify_password(OLDPASS, NEWPASS, modify_irods_authentication_file = modify_option)
82+
d['password'] = NEWPASS
83+
with iRODSSession(**d) as session:
84+
self.do_something(session) # can we still do stuff with the final value of the password?
85+
finally:
86+
shutil.rmtree(ENV_DIR)
87+
ses.users.remove('alice')
88+
89+
def test_modify_password_with_incorrect_old_value__328(self):
90+
ses = self.sess
91+
if ses.users.get( ses.username ).type != 'rodsadmin':
92+
self.skipTest( 'Only a rodsadmin may run this test.')
93+
OLDPASS = 'apass'
94+
NEWPASS = 'newpass'
95+
ENV_DIR = tempfile.mkdtemp()
96+
try:
97+
ses.users.create('alice', 'rodsuser')
98+
ses.users.modify('alice', 'password', OLDPASS)
99+
d = dict(password = OLDPASS, user = 'alice', host = ses.host, port = ses.port, zone = ses.zone)
100+
(alice_env, alice_auth) = helpers.make_environment_and_auth_files(ENV_DIR, **d)
101+
session_factories = [
102+
(lambda: iRODSSession(**d)),
103+
(lambda: helpers.make_session( irods_env_file = alice_env, irods_authentication_file = alice_auth)),
104+
]
105+
for factory in session_factories:
106+
with factory() as alice_ses:
107+
alice = alice_ses.users.get(alice_ses.username)
108+
with self.assertRaises( ex.CAT_PASSWORD_ENCODING_ERROR ):
109+
alice.modify_password(OLDPASS + ".", NEWPASS)
110+
with iRODSSession(**d) as alice_ses:
111+
self.do_something(alice_ses)
112+
finally:
113+
shutil.rmtree(ENV_DIR)
114+
ses.users.remove('alice')
115+
23116
def test_create_group(self):
24117
group_name = "test_group"
25118

irods/user.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ def metadata(self):
4444
self.manager.sess.metadata, User, self.name)
4545
return self._meta
4646

47+
def modify_password(self, old_value, new_value, modify_irods_authentication_file = False):
48+
self.manager.modify_password(old_value,
49+
new_value,
50+
modify_irods_authentication_file = modify_irods_authentication_file)
51+
4752
def modify(self, *args, **kwargs):
4853
self.manager.modify(self.name, *args, **kwargs)
4954

0 commit comments

Comments
 (0)