-
Notifications
You must be signed in to change notification settings - Fork 2k
/
authz.py
483 lines (413 loc) · 18.1 KB
/
authz.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
'''For an overview of CKAN authorization system and model see
doc/authorization.rst.
'''
from meta import *
from core import *
from package import *
from group import Group
from types import make_uuid
from user import User
from core import System
from authorization_group import AuthorizationGroup, authorization_group_table
PSEUDO_USER__LOGGED_IN = u'logged_in'
PSEUDO_USER__VISITOR = u'visitor'
class NotRealUserException(Exception):
pass
## ======================================
## Action and Role Enums
class Enum(object):
@classmethod
def is_valid(cls, val):
return val in cls.get_all()
@classmethod
def get_all(cls):
if not hasattr(cls, '_all_items'):
vals = []
for key, val in cls.__dict__.items():
if not key.startswith('_'):
vals.append(val)
cls._all_items = vals
return cls._all_items
class Action(Enum):
EDIT = u'edit'
CHANGE_STATE = u'change-state'
READ = u'read'
PURGE = u'purge'
EDIT_PERMISSIONS = u'edit-permissions'
PACKAGE_CREATE = u'create-package'
GROUP_CREATE = u'create-group'
AUTHZ_GROUP_CREATE = u'create-authorization-group'
SITE_READ = u'read-site'
USER_READ = u'read-user'
USER_CREATE = u'create-user'
class Role(Enum):
ADMIN = u'admin'
EDITOR = u'editor'
ANON_EDITOR = u'anon_editor'
READER = u'reader'
# These define what is meant by 'editor' and 'reader' for all ckan
# instances - locked down or otherwise. They get refreshed on every db_upgrade.
# So if you want to lock down an ckan instance, change Visitor and LoggedIn
# to have a new role which for which you can allow your customised actions.
default_role_actions = [
(Role.EDITOR, Action.EDIT),
(Role.EDITOR, Action.PACKAGE_CREATE),
(Role.EDITOR, Action.GROUP_CREATE),
(Role.EDITOR, Action.AUTHZ_GROUP_CREATE),
(Role.EDITOR, Action.USER_CREATE),
(Role.EDITOR, Action.USER_READ),
(Role.EDITOR, Action.SITE_READ),
(Role.EDITOR, Action.READ),
(Role.ANON_EDITOR, Action.EDIT),
(Role.ANON_EDITOR, Action.PACKAGE_CREATE),
(Role.ANON_EDITOR, Action.USER_CREATE),
(Role.ANON_EDITOR, Action.USER_READ),
(Role.ANON_EDITOR, Action.SITE_READ),
(Role.ANON_EDITOR, Action.READ),
(Role.READER, Action.USER_CREATE),
(Role.READER, Action.USER_READ),
(Role.READER, Action.SITE_READ),
(Role.READER, Action.READ),
]
## ======================================
## Table Definitions
role_action_table = Table('role_action', metadata,
Column('id', UnicodeText, primary_key=True, default=make_uuid),
Column('role', UnicodeText),
Column('context', UnicodeText, nullable=False),
Column('action', UnicodeText),
)
user_object_role_table = Table('user_object_role', metadata,
Column('id', UnicodeText, primary_key=True, default=make_uuid),
Column('user_id', UnicodeText, ForeignKey('user.id'), nullable=True),
Column('authorized_group_id', UnicodeText, ForeignKey('authorization_group.id'), nullable=True),
Column('context', UnicodeText, nullable=False),
Column('role', UnicodeText)
)
package_role_table = Table('package_role', metadata,
Column('user_object_role_id', UnicodeText, ForeignKey('user_object_role.id'), primary_key=True),
Column('package_id', UnicodeText, ForeignKey('package.id')),
)
group_role_table = Table('group_role', metadata,
Column('user_object_role_id', UnicodeText, ForeignKey('user_object_role.id'), primary_key=True),
Column('group_id', UnicodeText, ForeignKey('group.id')),
)
authorization_group_role_table = Table('authorization_group_role', metadata,
Column('user_object_role_id', UnicodeText, ForeignKey('user_object_role.id'), primary_key=True),
Column('authorization_group_id', UnicodeText, ForeignKey('authorization_group.id')),
)
system_role_table = Table('system_role', metadata,
Column('user_object_role_id', UnicodeText, ForeignKey('user_object_role.id'), primary_key=True),
)
class RoleAction(DomainObject):
def __repr__(self):
return '<%s role="%s" action="%s" context="%s">' % \
(self.__class__.__name__, self.role, self.action, self.context)
# dictionary mapping protected objects (e.g. Package) to related ObjectRole
protected_objects = {}
class UserObjectRole(DomainObject):
name = None
protected_object = None
def __repr__(self):
if self.user:
return '<%s user="%s" role="%s" context="%s">' % \
(self.__class__.__name__, self.user.name, self.role, self.context)
elif self.authorized_group:
return '<%s group="%s" role="%s" context="%s">' % \
(self.__class__.__name__, self.authorized_group.name, self.role, self.context)
else:
assert False, "UserObjectRole is neither for an authzgroup or for a user"
@classmethod
def get_object_role_class(cls, domain_obj):
protected_object = protected_objects.get(domain_obj.__class__, None)
if protected_object is None:
# TODO: make into an authz exception
msg = '%s is not a protected object, i.e. a subject of authorization' % domain_obj
raise Exception(msg)
else:
return protected_object
@classmethod
def user_has_role(cls, user, role, domain_obj):
assert isinstance(user, User), user
q = cls._user_query(user, role, domain_obj)
return q.count() == 1
@classmethod
def authorization_group_has_role(cls, authorized_group, role, domain_obj):
assert isinstance(authorized_group, AuthorizationGroup), authorized_group
q = cls._authorized_group_query(authorized_group, role, domain_obj)
return q.count() == 1
@classmethod
def _user_query(cls, user, role, domain_obj):
q = Session.query(cls).filter_by(role=role)
# some protected objects are not "contextual"
if cls.name is not None:
# e.g. filter_by(package=domain_obj)
q = q.filter_by(**dict({cls.name: domain_obj}))
q = q.filter_by(user=user)
return q
@classmethod
def _authorized_group_query(cls, authorized_group, role, domain_obj):
q = Session.query(cls).filter_by(role=role)
# some protected objects are not "contextual"
if cls.name is not None:
# e.g. filter_by(package=domain_obj)
q = q.filter_by(**dict({cls.name: domain_obj}))
q = q.filter_by(authorized_group=authorized_group)
return q
@classmethod
def add_user_to_role(cls, user, role, domain_obj):
'''NB: Leaves the caller to commit the change. If called twice without a
commit, will add the role to the database twice. Since some other
functions count the number of occurrences, that leaves a fairly obvious
bug. But adding a commit here seems to break various tests.
So don't call this twice without committing, I guess...
'''
# Here we're trying to guard against adding the same role twice, but
# that won't work if the transaction hasn't been committed yet, which allows a role to be added twice (you can do this from the interface)
if cls.user_has_role(user, role, domain_obj):
return
objectrole = cls(role=role, user=user)
if cls.name is not None:
setattr(objectrole, cls.name, domain_obj)
Session.add(objectrole)
@classmethod
def add_authorization_group_to_role(cls, authorization_group, role, domain_obj):
'''NB: Leaves the caller to commit the change. If called twice without a
commit, will add the role to the database twice. Since some other
functions count the number of occurrences, that leaves a fairly obvious
bug. But adding a commit here seems to break various tests.
So don\'t call this twice without committing, I guess...
'''
if cls.authorization_group_has_role(authorization_group, role, domain_obj):
return
objectrole = cls(role=role, authorized_group=authorization_group)
if cls.name is not None:
setattr(objectrole, cls.name, domain_obj)
Session.add(objectrole)
@classmethod
def remove_user_from_role(cls, user, role, domain_obj):
q = cls._user_query(user, role, domain_obj)
for uo_role in q.all():
Session.delete(uo_role)
Session.commit()
Session.remove()
@classmethod
def remove_authorization_group_from_role(cls, authorization_group, role, domain_obj):
q = cls._authorized_group_query(authorization_group, role, domain_obj)
for ago_role in q.all():
Session.delete(ago_role)
Session.commit()
Session.remove()
class PackageRole(UserObjectRole):
protected_object = Package
name = 'package'
protected_objects[PackageRole.protected_object] = PackageRole
class GroupRole(UserObjectRole):
protected_object = Group
name = 'group'
protected_objects[GroupRole.protected_object] = GroupRole
class AuthorizationGroupRole(UserObjectRole):
protected_object = AuthorizationGroup
name = 'authorization_group'
protected_objects[AuthorizationGroupRole.protected_object] = AuthorizationGroupRole
class SystemRole(UserObjectRole):
protected_object = System
name = None
protected_objects[SystemRole.protected_object] = SystemRole
## ======================================
## Helpers
def user_has_role(user, role, domain_obj):
objectrole = UserObjectRole.get_object_role_class(domain_obj)
return objectrole.user_has_role(user, role, domain_obj)
def add_user_to_role(user, role, domain_obj):
objectrole = UserObjectRole.get_object_role_class(domain_obj)
objectrole.add_user_to_role(user, role, domain_obj)
def remove_user_from_role(user, role, domain_obj):
objectrole = UserObjectRole.get_object_role_class(domain_obj)
objectrole.remove_user_from_role(user, role, domain_obj)
def authorization_group_has_role(authorization_group, role, domain_obj):
objectrole = UserObjectRole.get_object_role_class(domain_obj)
return objectrole.authorization_group_has_role(authorization_group, role, domain_obj)
def add_authorization_group_to_role(authorization_group, role, domain_obj):
objectrole = UserObjectRole.get_object_role_class(domain_obj)
objectrole.add_authorization_group_to_role(authorization_group, role, domain_obj)
def remove_authorization_group_from_role(authorization_group, role, domain_obj):
objectrole = UserObjectRole.get_object_role_class(domain_obj)
objectrole.remove_authorization_group_from_role(authorization_group, role, domain_obj)
def init_authz_configuration_data():
setup_default_user_roles(System())
Session.commit()
Session.remove()
def init_authz_const_data():
'''Setup all default role-actions.
These should be the same for all CKAN instances. Make custom roles if
you want to divert from these.
Note that Role.ADMIN can already do anything - hardcoded in.
'''
for role, action in default_role_actions:
ra = Session.query(RoleAction).filter_by(role=role, action=action).first()
if ra is not None: continue
ra = RoleAction(role=role, context=u'', action=action)
Session.add(ra)
Session.commit()
Session.remove()
## TODO: this should be in ckan/authz.py
def setup_user_roles(domain_object, visitor_roles, logged_in_roles, admins=[]):
'''NB: leaves caller to commit change'''
assert type(admins) == type([])
admin_roles = [Role.ADMIN]
visitor = User.by_name(PSEUDO_USER__VISITOR)
assert visitor
for role in visitor_roles:
add_user_to_role(visitor, role, domain_object)
logged_in = User.by_name(PSEUDO_USER__LOGGED_IN)
assert logged_in
for role in logged_in_roles:
add_user_to_role(logged_in, role, domain_object)
for admin in admins:
# not sure if admin would reasonably by None
if admin is not None:
assert isinstance(admin, User), admin
if admin.name in (PSEUDO_USER__LOGGED_IN, PSEUDO_USER__VISITOR):
raise NotRealUserException('Invalid user for domain object admin %r' % admin.name)
for role in admin_roles:
add_user_to_role(admin, role, domain_object)
def give_all_packages_default_user_roles():
# if this command gives an exception, you probably
# forgot to do 'paster db init'
pkgs = Session.query(Package).all()
for pkg in pkgs:
print pkg
# weird - should already be in session but complains w/o this
Session.add(pkg)
if len(pkg.roles) > 0:
print 'Skipping (already has roles): %s' % pkg.name
continue
# work out the authors and make them admins
admins = []
revs = pkg.all_revisions
for rev in revs:
if rev.revision.author:
# rev author is not Unicode!!
user = User.by_name(unicode(rev.revision.author))
if user:
admins.append(user)
# remove duplicates
admins = list(set(admins))
# gives default permissions
print 'Creating default user for for %s with admins %s' % (pkg.name, admins)
setup_default_user_roles(pkg, admins)
# default user roles - used when the config doesn\'t specify them
default_default_user_roles = {
'Package': {"visitor": ["editor"], "logged_in": ["editor"]},
'Group': {"visitor": ["reader"], "logged_in": ["reader"]},
'System': {"visitor": ["anon_editor"], "logged_in": ["editor"]},
'AuthorizationGroup': {"visitor": ["reader"], "logged_in": ["reader"]},
}
global _default_user_roles_cache
_default_user_roles_cache = {}
def get_default_user_roles(domain_object):
# TODO: Should this func go in lib rather than model now?
from ckan.lib.helpers import json
from pylons import config
def _get_default_user_roles(domain_object):
config_key = 'ckan.default_roles.%s' % obj_type
user_roles_json = config.get(config_key)
if user_roles_json is None:
user_roles_str = default_default_user_roles[obj_type]
else:
user_roles_str = json.loads(user_roles_json) if user_roles_json else {}
unknown_keys = set(user_roles_str.keys()) - set(('visitor', 'logged_in'))
assert not unknown_keys, 'Auth config for %r has unknown key %r' % \
(domain_object, unknown_keys)
user_roles_ = {}
for user in ('visitor', 'logged_in'):
roles_str = user_roles_str.get(user, [])
user_roles_[user] = [getattr(Role, role_str.upper()) for role_str in roles_str]
return user_roles_
obj_type = domain_object.__class__.__name__
global _default_user_roles_cache
if not _default_user_roles_cache.has_key(domain_object):
_default_user_roles_cache[domain_object] = _get_default_user_roles(domain_object)
return _default_user_roles_cache[domain_object]
def setup_default_user_roles(domain_object, admins=[]):
''' sets up roles for visitor, logged-in user and any admins provided
@param admins - a list of User objects
NB: leaves caller to commit change.
'''
assert isinstance(domain_object, (Package, Group, System, AuthorizationGroup)), domain_object
assert isinstance(admins, list)
user_roles_ = get_default_user_roles(domain_object)
setup_user_roles(domain_object,
user_roles_['visitor'],
user_roles_['logged_in'],
admins)
def clear_user_roles(domain_object):
assert isinstance(domain_object, DomainObject)
if isinstance(domain_object, Package):
q = Session.query(PackageRole).filter_by(package=domain_object)
elif isinstance(domain_object, Group):
q = Session.query(GroupRole).filter_by(group=domain_object)
else:
raise NotImplementedError()
user_roles = q.all()
for user_role in user_roles:
Session.delete(user_role)
## ======================================
## Mappers
mapper(RoleAction, role_action_table)
mapper(UserObjectRole, user_object_role_table,
polymorphic_on=user_object_role_table.c.context,
polymorphic_identity=u'user_object',
properties={
'user': orm.relation(User,
backref=orm.backref('roles',
cascade='all, delete, delete-orphan'
)
),
'authorized_group': orm.relation(AuthorizationGroup,
backref=orm.backref('authorized_roles',
cascade='all, delete, delete-orphan'
)
)
},
order_by=[user_object_role_table.c.id],
)
mapper(PackageRole, package_role_table, inherits=UserObjectRole,
polymorphic_identity=unicode(Package.__name__),
properties={
'package': orm.relation(Package,
backref=orm.backref('roles',
cascade='all, delete, delete-orphan'
)
),
},
order_by=[package_role_table.c.user_object_role_id],
)
mapper(GroupRole, group_role_table, inherits=UserObjectRole,
polymorphic_identity=unicode(Group.__name__),
properties={
'group': orm.relation(Group,
backref=orm.backref('roles',
cascade='all, delete, delete-orphan'
),
)
},
order_by=[group_role_table.c.user_object_role_id],
)
mapper(AuthorizationGroupRole, authorization_group_role_table, inherits=UserObjectRole,
polymorphic_identity=unicode(AuthorizationGroup.__name__),
properties={
'authorization_group': orm.relation(AuthorizationGroup,
backref=orm.backref('roles',
primaryjoin=authorization_group_table.c.id==authorization_group_role_table.c.authorization_group_id,
cascade='all, delete, delete-orphan'
),
)
},
order_by=[authorization_group_role_table.c.user_object_role_id],
)
mapper(SystemRole, system_role_table, inherits=UserObjectRole,
polymorphic_identity=unicode(System.__name__),
order_by=[system_role_table.c.user_object_role_id],
)