Skip to content

Commit 8551aa5

Browse files
d-w-moorealanking
authored andcommitted
[#426][#428][#429] groupadmin capabilities update
The Python client now provides: * [426][428] the complete capabilities offered in "igroupadmin", including mkgroup, atg, rfg, and mkuser. * [429] "session.groups" as a synonym for "session.user_groups" -- the latter now being deprecated. (Accordingly, we rename "UserGroup" to simply "Group" where used in class names.)
1 parent 202144c commit 8551aa5

11 files changed

Lines changed: 307 additions & 76 deletions

File tree

README.rst

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,9 +1064,9 @@ Listing Users and Groups ; calculating Group Membership
10641064
iRODS tracks groups and users using two tables, R_USER_MAIN and R_USER_GROUP.
10651065
Under this database schema, all "user groups" are also users:
10661066

1067-
>>> from irods.models import User, UserGroup
1067+
>>> from irods.models import User, Group
10681068
>>> from pprint import pprint
1069-
>>> pprint(list( [ (x[User.id], x[User.name]) for x in session.query(User) ] ))
1069+
>>> pprint(list((x[User.id], x[User.name]) for x in session.query(User)))
10701070
[(10048, 'alice'),
10711071
(10001, 'rodsadmin'),
10721072
(13187, 'bobby'),
@@ -1083,20 +1083,20 @@ user ID that iRODS internally recognizes as a "Group":
10831083
>>> [x[User.name] for x in groups]
10841084
['collab', 'public', 'rodsadmin', 'empty']
10851085

1086-
Since we can instantiate iRODSUserGroup and iRODSUser objects directly from the rows of
1086+
Since we can instantiate iRODSGroup and iRODSUser objects directly from the rows of
10871087
a general query on the corresponding tables, it is also straightforward to trace out
10881088
the groups' memberships:
10891089

1090-
>>> from irods.user import iRODSUser, iRODSUserGroup
1091-
>>> grp_usr_mapping = [ (iRODSUserGroup ( session.user_groups, result), iRODSUser (session.users, result)) \
1092-
... for result in session.query(UserGroup,User) ]
1090+
>>> from irods.user import iRODSUser, iRODSGroup
1091+
>>> grp_usr_mapping = [ (iRODSGroup(session.groups, result), iRODSUser(session.users, result)) \
1092+
... for result in session.query(Group,User) ]
10931093
>>> pprint( [ (x,y) for x,y in grp_usr_mapping if x.id != y.id ] )
1094-
[(<iRODSUserGroup 10045 collab>, <iRODSUser 10048 alice rodsuser tempZone>),
1095-
(<iRODSUserGroup 10001 rodsadmin>, <iRODSUser 10003 rods rodsadmin tempZone>),
1096-
(<iRODSUserGroup 10002 public>, <iRODSUser 10003 rods rodsadmin tempZone>),
1097-
(<iRODSUserGroup 10002 public>, <iRODSUser 10048 alice rodsuser tempZone>),
1098-
(<iRODSUserGroup 10045 collab>, <iRODSUser 13187 bobby rodsuser tempZone>),
1099-
(<iRODSUserGroup 10002 public>, <iRODSUser 13187 bobby rodsuser tempZone>)]
1094+
[(<iRODSGroup 10045 collab>, <iRODSUser 10048 alice rodsuser tempZone>),
1095+
(<iRODSGroup 10001 rodsadmin>, <iRODSUser 10003 rods rodsadmin tempZone>),
1096+
(<iRODSGroup 10002 public>, <iRODSUser 10003 rods rodsadmin tempZone>),
1097+
(<iRODSGroup 10002 public>, <iRODSUser 10048 alice rodsuser tempZone>),
1098+
(<iRODSGroup 10045 collab>, <iRODSUser 13187 bobby rodsuser tempZone>),
1099+
(<iRODSGroup 10002 public>, <iRODSUser 13187 bobby rodsuser tempZone>)]
11001100

11011101
(Note that in general queries, fields cannot be compared to each other, only to literal constants; thus
11021102
the '!=' comparison in the Python list comprehension.)
@@ -1108,6 +1108,24 @@ users (those whose User.type is 'rodsadmin' or 'rodsuser'). The empty group ('em
11081108
members, so it doesn't show up in our final list.
11091109

11101110

1111+
Group Administrator Capabilities
1112+
--------------------------------
1113+
With v1.1.7, PRC acquires the full range of abilities possessed by the igroupadmin command.
1114+
1115+
Firstly, a groupadmin may invoke methods to create groups, and may add users to, or remove them from, any
1116+
group to which they themselves belong:
1117+
1118+
>>> session.groups.create('lab')
1119+
>>> session.groups.addmember('lab',session.username) # allow self to administer group
1120+
>>> session.groups.addmember('lab','otheruser')
1121+
>>> session.groups.removemember('lab','otheruser')
1122+
1123+
In addition, a groupadmin may also create accounts for new users and enable their logins by initializing
1124+
a native password for them:
1125+
1126+
>>> session.users.create_with_password('alice', 'change_me')
1127+
1128+
11111129
iRODS Permissions (ACLs)
11121130
------------------------
11131131

irods/exception.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ class UserDoesNotExist(DoesNotExist):
3939
pass
4040

4141

42-
class UserGroupDoesNotExist(DoesNotExist):
42+
class GroupDoesNotExist(DoesNotExist):
4343
pass
4444

45+
# NOTE: Everything of the form *UserGroup* is deprecated.
46+
UserGroupDoesNotExist = GroupDoesNotExist
4547

4648
class ResourceDoesNotExist(DoesNotExist):
4749
pass

irods/manager/user_manager.py

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from __future__ import absolute_import
22
import logging
33
import os
4+
import warnings
45

5-
from irods.models import User, UserGroup
6+
from irods.models import User, Group
67
from irods.manager import Manager
78
from irods.message import UserAdminRequest, GeneralAdminRequest, iRODSMessage, GetTempPasswordForOtherRequest, GetTempPasswordForOtherOut
8-
from irods.exception import UserDoesNotExist, UserGroupDoesNotExist, NoResultFound, CAT_SQL_ERR
9+
from irods.exception import UserDoesNotExist, GroupDoesNotExist, NoResultFound, CAT_SQL_ERR
910
from irods.api_number import api_number
10-
from irods.user import iRODSUser, iRODSUserGroup
11+
from irods.user import iRODSUser, iRODSGroup
1112
import irods.password_obfuscation as obf
1213

1314
logger = logging.getLogger(__name__)
@@ -27,6 +28,24 @@ def get(self, user_name, user_zone=""):
2728
raise UserDoesNotExist()
2829
return iRODSUser(self, result)
2930

31+
def create_with_password(self, user_name, password, user_zone = ''):
32+
"""This method can be used by a groupadmin to initialize the password field while creating the new user.
33+
(This is necessary since group administrators may not change the password of an existing user.)
34+
"""
35+
36+
message_body = UserAdminRequest(
37+
"mkuser", user_name, "" if not password else \
38+
obf.obfuscate_new_password_with_key(password, self.sess.pool.account.password)
39+
)
40+
request = iRODSMessage("RODS_API_REQ", msg=message_body,
41+
int_info=api_number['USER_ADMIN_AN'])
42+
with self.sess.pool.get_connection() as conn:
43+
conn.send(request)
44+
response = conn.recv()
45+
logger.debug(response.int_info)
46+
return self.get(user_name, user_zone)
47+
48+
3049
def create(self, user_name, user_type, user_zone="", auth_str=""):
3150
message_body = GeneralAdminRequest(
3251
"add",
@@ -176,36 +195,60 @@ def modify(self, user_name, option, new_value, user_zone=""):
176195
logger.debug(response.int_info)
177196

178197

179-
class UserGroupManager(UserManager):
198+
class GroupManager(UserManager):
180199

181200
def get(self, name, user_zone=""):
182-
query = self.sess.query(UserGroup).filter(UserGroup.name == name)
201+
query = self.sess.query(Group).filter(Group.name == name)
183202

184203
try:
185204
result = query.one()
186205
except NoResultFound:
187-
raise UserGroupDoesNotExist()
188-
return iRODSUserGroup(self, result)
206+
raise GroupDoesNotExist()
207+
return iRODSGroup(self, result)
189208

190-
def create(self, name, user_type='rodsgroup', user_zone="", auth_str="", group_admin=False):
191-
if group_admin:
192-
message_body = UserAdminRequest(
209+
def _api_info(self, group_admin_flag):
210+
"""
211+
If group_admin_flag is: then use UserAdminRequest API:
212+
--------------------------- ------------------------------
213+
True always
214+
False (user_groups default) never
215+
None (groups default) when user type is groupadmin
216+
"""
217+
218+
sess = self.sess
219+
if group_admin_flag or (group_admin_flag is not False and sess.users.get(sess.username).type == 'groupadmin'):
220+
return (UserAdminRequest, 'USER_ADMIN_AN')
221+
return (GeneralAdminRequest, 'GENERAL_ADMIN_AN')
222+
223+
def create(self, name,
224+
user_type = 'rodsgroup',
225+
user_zone = "",
226+
auth_str = "",
227+
group_admin = False, **options):
228+
229+
if not options.pop('suppress_deprecation_warning', False):
230+
warnings.warn('Use of session.user_groups is deprecated in v1.1.7 - prefer session.groups',
231+
DeprecationWarning, stacklevel = 2)
232+
233+
(MessageClass, api_key) = self._api_info(group_admin)
234+
api_to_use = api_number[api_key]
235+
236+
if MessageClass is UserAdminRequest:
237+
message_body = MessageClass(
193238
"mkgroup",
194239
name,
195240
user_type,
196241
user_zone
197242
)
198-
api_to_use = api_number['USER_ADMIN_AN']
199243
else:
200-
message_body = GeneralAdminRequest(
244+
message_body = MessageClass(
201245
"add",
202246
"user",
203247
name,
204248
user_type,
205249
"",
206250
""
207251
)
208-
api_to_use = api_number['GENERAL_ADMIN_AN']
209252
request = iRODSMessage("RODS_API_REQ", msg=message_body,
210253
int_info=api_to_use)
211254
with self.sess.pool.get_connection() as conn:
@@ -216,11 +259,21 @@ def create(self, name, user_type='rodsgroup', user_zone="", auth_str="", group_a
216259

217260
def getmembers(self, name):
218261
results = self.sess.query(User).filter(
219-
User.type != 'rodsgroup', UserGroup.name == name)
262+
User.type != 'rodsgroup', Group.name == name)
220263
return [iRODSUser(self, row) for row in results]
221264

222-
def addmember(self, group_name, user_name, user_zone=""):
223-
message_body = GeneralAdminRequest(
265+
def addmember(self, group_name,
266+
user_name,
267+
user_zone = "",
268+
group_admin = False, **options):
269+
270+
if not options.pop('suppress_deprecation_warning', False):
271+
warnings.warn('Use of session.user_groups is deprecated in v1.1.7 - prefer session.groups',
272+
DeprecationWarning, stacklevel = 2)
273+
274+
(MessageClass, api_key) = self._api_info(group_admin)
275+
276+
message_body = MessageClass(
224277
"modify",
225278
"group",
226279
group_name,
@@ -229,14 +282,25 @@ def addmember(self, group_name, user_name, user_zone=""):
229282
user_zone
230283
)
231284
request = iRODSMessage("RODS_API_REQ", msg=message_body,
232-
int_info=api_number['GENERAL_ADMIN_AN'])
285+
int_info=api_number[api_key])
233286
with self.sess.pool.get_connection() as conn:
234287
conn.send(request)
235288
response = conn.recv()
236289
logger.debug(response.int_info)
237290

238-
def removemember(self, group_name, user_name, user_zone=""):
239-
message_body = GeneralAdminRequest(
291+
def removemember(self,
292+
group_name,
293+
user_name,
294+
user_zone = "",
295+
group_admin = False, **options):
296+
297+
if not options.pop('suppress_deprecation_warning', False):
298+
warnings.warn('Use of session.user_groups is deprecated in v1.1.7 - prefer session.groups',
299+
DeprecationWarning, stacklevel = 2)
300+
301+
(MessageClass, api_key) = self._api_info(group_admin)
302+
303+
message_body = MessageClass(
240304
"modify",
241305
"group",
242306
group_name,
@@ -245,8 +309,12 @@ def removemember(self, group_name, user_name, user_zone=""):
245309
user_zone
246310
)
247311
request = iRODSMessage("RODS_API_REQ", msg=message_body,
248-
int_info=api_number['GENERAL_ADMIN_AN'])
312+
int_info=api_number[api_key])
249313
with self.sess.pool.get_connection() as conn:
250314
conn.send(request)
251315
response = conn.recv()
252316
logger.debug(response.int_info)
317+
318+
319+
# NOTE: Everything of the form *UserGroup* is deprecated.
320+
UserGroupManager = GroupManager

irods/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,12 @@ class CollectionUser(Model):
6666
zone = Column(String, 'COL_COLL_USER_ZONE', 1301)
6767

6868

69-
class UserGroup(Model):
69+
class Group(Model):
7070
id = Column(Integer, 'USER_GROUP_ID', 900)
7171
name = Column(String, 'USER_GROUP_NAME', 901)
72+
73+
# The UserGroup model-class is now renamed Group, but we'll keep the deprecated name around for now.
74+
UserGroup = Group
7275

7376

7477
class Resource(Model):

irods/password_obfuscation.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,15 @@ def scramble_v2(s, first_key, second_key):
288288

289289
return scramble(to_scramble, key=hashed_key, scramble_prefix='', block_chaining=True)
290290

291+
def obfuscate_new_password_with_key(new_password, obfuscation_key):
292+
lcopy = MAX_PASSWORD_LENGTH - 10 - len(new_password)
293+
new_password = new_password[:MAX_PASSWORD_LENGTH]
294+
if lcopy > 15:
295+
# https://github.com/irods/irods/blob/4.2.1/plugins/database/src/db_plugin.cpp#L1094-L1095
296+
padding = '1gCBizHWbwIYyWLoysGzTe6SyzqFKMniZX05faZHWAwQKXf6Fs'
297+
new_password += padding[:lcopy]
298+
return scramble(new_password, obfuscation_key, scramble_prefix='')
299+
291300
# port of https://github.com/irods/irods_client_icommands/blob/4.2.1/src/iadmin.cpp#L878-L930
292301
def obfuscate_new_password(new, old, signature):
293302
pwd_len = len(new)

irods/session.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from irods.manager.data_object_manager import DataObjectManager
1313
from irods.manager.metadata_manager import MetadataManager
1414
from irods.manager.access_manager import AccessManager
15-
from irods.manager.user_manager import UserManager, UserGroupManager
15+
from irods.manager.user_manager import UserManager, GroupManager
1616
from irods.manager.resource_manager import ResourceManager
1717
from irods.manager.zone_manager import ZoneManager
1818
from irods.exception import NetworkException
@@ -67,6 +67,51 @@ def available_permissions(self):
6767
self.__access = _iRODSAccess_pre_4_3_0 if self.server_version < (4,3) else iRODSAccess
6868
return self.__access
6969

70+
@property
71+
def groups(self):
72+
class _GroupManager(self.user_groups.__class__):
73+
74+
def create(self, name,
75+
group_admin = None): # NB new default (see user_groups manager and i/f, with False as default)
76+
77+
user_type = 'rodsgroup' # These are no longer parameters in the new interface, as they have no reason to vary.
78+
user_zone = "" # Groups (1) are always of type 'rodsgroup', (2) always belong to the local zone, and
79+
auth_str = "" # (3) do not authenticate.
80+
81+
return super(_GroupManager, self).create(name,
82+
user_type,
83+
user_zone,
84+
auth_str,
85+
group_admin,
86+
suppress_deprecation_warning = True)
87+
88+
def addmember(self, group_name,
89+
user_name,
90+
user_zone = "",
91+
group_admin = None):
92+
93+
return super(_GroupManager, self).addmember(group_name,
94+
user_name,
95+
user_zone,
96+
group_admin,
97+
suppress_deprecation_warning = True)
98+
99+
def removemember(self, group_name,
100+
user_name,
101+
user_zone = "",
102+
group_admin = None):
103+
104+
return super(_GroupManager, self).removemember(group_name,
105+
user_name,
106+
user_zone,
107+
group_admin,
108+
suppress_deprecation_warning = True)
109+
110+
_groups = getattr(self,'_groups',None)
111+
if not _groups:
112+
_groups = self._groups = _GroupManager(self.user_groups.sess)
113+
return self._groups
114+
70115
@property
71116
def acls(self):
72117
class ACLs(self.permissions.__class__):
@@ -95,7 +140,7 @@ def __init__(self, configure = True, auto_cleanup = True, **kwargs):
95140
self.metadata = MetadataManager(self)
96141
self.permissions = AccessManager(self)
97142
self.users = UserManager(self)
98-
self.user_groups = UserGroupManager(self)
143+
self.user_groups = GroupManager(self)
99144
self.resources = ResourceManager(self)
100145
self.zones = ZoneManager(self)
101146

irods/test/access_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,10 +249,10 @@ def test_raw_acls__207(self):
249249
data = helpers.make_object(self.sess,"/".join((self.coll_path,"test_obj")))
250250
eg = eu = fg = fu = None
251251
try:
252-
eg = self.sess.user_groups.create ('egrp')
252+
eg = self.sess.groups.create ('egrp')
253253
eu = self.sess.users.create ('edith','rodsuser')
254254
eg.addmember(eu.name,eu.zone)
255-
fg = self.sess.user_groups.create ('fgrp')
255+
fg = self.sess.groups.create ('fgrp')
256256
fu = self.sess.users.create ('frank','rodsuser')
257257
fg.addmember(fu.name,fu.zone)
258258
my_ownership = set([('own', self.sess.username, self.sess.zone)])
@@ -283,7 +283,7 @@ def test_ses_acls_data_and_collection_395_396(self):
283283

284284
self.alice = ses.users.create('alice','rodsuser')
285285
self.bob = ses.users.create('bob','rodsuser')
286-
self.team = ses.user_groups.create('team')
286+
self.team = ses.groups.create('team')
287287
self.team.addmember('bob')
288288
ses.users.modify('bob', 'password', 'bpass')
289289

0 commit comments

Comments
 (0)