Skip to content

Commit

Permalink
[#1163] Users can be deleted
Browse files Browse the repository at this point in the history
To do this, I've configured the User model to be stateful using vdm.sqlalchemy.
Right now, there're two states: active and deleted. If a user is deleted, he
can't login, and is unauthorized to do anything. She also doesn't appear in the
user's list anymore, but you can still access her profile page, if you know her
username.

If she was logged in when her user was deleted, the next time she goes into
CKAN, she'll be logged off. Unfortunately, there's not a useful message like
"Your user has been deleted." Yet.

There's no way to undelete a user, but it should be simply creating an
action to set her state to active.
  • Loading branch information
vitorbaptista committed Aug 16, 2013
1 parent 01a3c05 commit 7c60ba4
Show file tree
Hide file tree
Showing 21 changed files with 527 additions and 147 deletions.
1 change: 1 addition & 0 deletions ckan/config/routing.py
Expand Up @@ -359,6 +359,7 @@ def make_map():
action='followers', ckan_icon='group')
m.connect('user_edit', '/user/edit/{id:.*}', action='edit',
ckan_icon='cog')
m.connect('user_delete', '/user/delete/{id}', action='delete')
m.connect('/user/reset/{id:.*}', action='perform_reset')
m.connect('register', '/user/register', action='register')
m.connect('login', '/user/login', action='login')
Expand Down
15 changes: 15 additions & 0 deletions ckan/controllers/user.py
Expand Up @@ -178,6 +178,21 @@ def new(self, data=None, errors=None, error_summary=None):
c.form = render(self.new_user_form, extra_vars=vars)
return render('user/new.html')

def delete(self, id):
'''Delete user with id passed as parameter'''
context = {'model': model,
'session': model.Session,
'user': c.user}
data_dict = {'id': id}

try:
get_action('user_delete')(context, data_dict)
user_index = h.url_for(controller='user', action='index')
h.redirect_to(user_index)
except NotAuthorized:
msg = _('Unauthorized to delete user with id "{user_id}".')
abort(401, msg.format(user_id=id))

def _save_new(self, context):
try:
data_dict = logic.clean_dict(unflatten(
Expand Down
22 changes: 14 additions & 8 deletions ckan/lib/authenticator.py
Expand Up @@ -12,9 +12,9 @@ class OpenIDAuthenticator(object):

def authenticate(self, environ, identity):
if 'repoze.who.plugins.openid.userid' in identity:
openid = identity.get('repoze.who.plugins.openid.userid')
openid = identity['repoze.who.plugins.openid.userid']
user = User.by_openid(openid)
if user is None:
if user is None or user.is_deleted():
return None
else:
return user.name
Expand All @@ -25,14 +25,20 @@ class UsernamePasswordAuthenticator(object):
implements(IAuthenticator)

def authenticate(self, environ, identity):
if not 'login' in identity or not 'password' in identity:
if not ('login' in identity and 'password' in identity):
return None
user = User.by_name(identity.get('login'))

login = identity['login']
user = User.by_name(login)

if user is None:
log.debug('Login failed - username %r not found', identity.get('login'))
return None
if user.validate_password(identity.get('password')):
log.debug('Login failed - username %r not found', login)
elif user.is_deleted():
log.debug('Login as %r failed - user is deleted', login)
elif not user.validate_password(identity['password']):
log.debug('Login as %r failed - password not valid', login)
else:
return user.name
log.debug('Login as %r failed - password not valid', identity.get('login'))

return None

5 changes: 3 additions & 2 deletions ckan/lib/base.py
Expand Up @@ -290,8 +290,9 @@ def _identify_user_default(self):
if c.user:
c.user = c.user.decode('utf8')
c.userobj = model.User.by_name(c.user)
if c.userobj is None:
# This occurs when you are logged in, clean db
if c.userobj is None or c.userobj.is_deleted():
# This occurs when a user that was still logged in is deleted,
# or when you are logged in, clean db
# and then restart (or when you change your username)
# There is no user object, so even though repoze thinks you
# are logged in and your cookie has ckan_display_name, we
Expand Down
3 changes: 2 additions & 1 deletion ckan/lib/create_test_data.py
Expand Up @@ -519,8 +519,9 @@ def _create_user_without_commit(cls, name='', **user_dict):

@classmethod
def create_user(cls, name='', **kwargs):
cls._create_user_without_commit(name, **kwargs)
user = cls._create_user_without_commit(name, **kwargs)
model.Session.commit()
return user

@classmethod
def flag_for_deletion(cls, pkg_names=[], tag_names=[], group_names=[],
Expand Down
22 changes: 22 additions & 0 deletions ckan/logic/action/delete.py
Expand Up @@ -18,6 +18,28 @@
_get_or_bust = ckan.logic.get_or_bust
_get_action = ckan.logic.get_action

def user_delete(context, data_dict):
'''Delete a user.
Only sysadmins can delete users.
:param id: the id or usernamename of the user to delete
:type id: string
'''

_check_access('user_delete', context, data_dict)

model = context['model']
user_id = _get_or_bust(data_dict, 'id')
user = model.User.get(user_id)

if user is None:
raise NotFound('User "{id}" was not found.'.format(id=user_id))

user.delete()
model.repo.commit()


def package_delete(context, data_dict):
'''Delete a dataset (package).
Expand Down
8 changes: 7 additions & 1 deletion ckan/logic/action/get.py
Expand Up @@ -7,6 +7,7 @@

from pylons import config
import sqlalchemy
import vdm.sqlalchemy

import ckan.lib.dictization
import ckan.logic as logic
Expand Down Expand Up @@ -664,6 +665,9 @@ def user_list(context, data_dict):
else_=model.User.fullname)
)

# Filter deleted users
query = query.filter(model.User.state != vdm.sqlalchemy.State.DELETED)

## hack for pagination
if context.get('return_query'):
return query
Expand Down Expand Up @@ -1173,7 +1177,9 @@ def user_autocomplete(context, data_dict):
q = data_dict['q']
limit = data_dict.get('limit', 20)

query = model.User.search(q).limit(limit)
query = model.User.search(q)
query = query.filter(model.User.state != vdm.sqlalchemy.State.DELETED)
query = query.limit(limit)

user_list = []
for user in query.all():
Expand Down
4 changes: 4 additions & 0 deletions ckan/logic/auth/delete.py
Expand Up @@ -4,6 +4,10 @@
from ckan.logic.auth import get_resource_object
from ckan.lib.base import _

def user_delete(context, data_dict):
# Only sysadmins are authorized to purge organizations.
return {'success': False}

def package_delete(context, data_dict):
user = context['user']
package = get_package_object(context, data_dict)
Expand Down
17 changes: 17 additions & 0 deletions ckan/migration/versions/070_add_state_column_to_user_table.py
@@ -0,0 +1,17 @@
import vdm.sqlalchemy


def upgrade(migrate_engine):
migrate_engine.execute(
'''
ALTER TABLE "user" ADD COLUMN "state" text NOT NULL DEFAULT '%s'
''' % vdm.sqlalchemy.State.ACTIVE
)


def downgrade(migrate_engine):
migrate_engine.exeecute(
'''
ALTER TABLE "user" DROP COLUMN "state"
'''
)

0 comments on commit 7c60ba4

Please sign in to comment.