def _send_token(req, info): """Send an email token if the password was forgotten. _send_token(req, info) Returns a status page. """ import logging import os import manage_kbasix import manage_users import smtplib import aux from email.mime.text import MIMEText logging.debug('Starting forgotten password procedure for "%s"' % \ info['login_name']) if not manage_users._info(info['login_name']) or \ manage_users._info(info['login_name'])['locked'] or \ info['login_name'] in info['banned_logins']: info['class'] = 'fail' info['main_header'] = aux._make_header(info) info['title'] = 'Password reset' info['details'] = 'Either that user name was not found, you are \ not allowed into the system, or the account has yet to be activated.' info['status_button_1'] = '' info['status_button_2'] = '' logging.debug('Token not sent to "%s" because "%s"' % \ (info['login_name'], info['details'])) return aux._fill_page(info['status_page_'], info) uid = manage_users._info(info['login_name'])['uid'] user_email = manage_kbasix._account_info(info['login_name'], \ 'profile')['user_email'] # The token will only allow access to the profile module (which allows # to change the password). It should work from any IP address. token = manage_kbasix._create_token(req, uid, ip_set=False, \ access='profile.py') www_path = os.path.dirname((req.subprocess_env['SCRIPT_NAME'])) info['profile_url'] = \ req.construct_url(www_path + \ '/profile.py/process?start&token=%s' % token) msg = MIMEText(aux._fill_str(info['reset_password_notice'], info)) msg['Subject'] = aux._fill_str(info['reset_password_subject'], info) msg['From'] = info['reset_password_from_email_'] msg['To'] = user_email s = smtplib.SMTP(info['smtp_host_']) s.sendmail(info['reset_password_from_email_'], [user_email], \ msg.as_string()) s.quit() info['class'] = 'success' info['main_header'] = aux._make_header(info) info['title'] = 'Password reset' info['details'] = aux._fill_str(info['reset_password_blurb'], info) info['status_button_1'] = '' info['status_button_2'] = '' logging.info('Token sent due to forgotten password by "%s" to "%s"' % \ (info['login_name'], user_email)) return aux._fill_page(info['status_page_'], info)
def _account_del(login_name, wipe=False): """Delete a KBasix user account. _account_del(login_name, wipe=False) If 'wipe' is True, the user files are deleted. Otherwise it is feasable to re-open the account (perhaps even under a different user name, if the former has been taken already). """ (OK, status) = _check_args(locals()) if not OK: raise AccountDelError(status) import manage_users user_name = _account_info(login_name, 'profile')['user_name'] uid = str(_info(login_name)['uid']) user_dir = os.path.join(users_root_dir_, uid) if not wipe: _account_mod(login_name, 'profile', {'login_name': '', \ 'user_name': ''}) (OK, status) = manage_users._del(login_name) if not OK: raise AccountDelError(status) # We have to remove the world symlink in case we want to re-use the # user name. try: os.remove(os.path.join(www_dir_, user_name)) except Exception as reason: raise AccountDelError(reason) if wipe: try: import shutil shutil.rmtree(user_dir) except Exception as reason: raise AccountDelError(reason) return
def _account_mod(login_name, ext, settings): """Modify a KBasix user account. _account_mod(login_name, ext, settings) The 'ext' can be 'profile' or 'prefs' depending if a user's profile information or preferences are being changed. The 'settings' is a dictionary. Nothing is returned. """ (OK, status) = _check_args(locals()) if not OK: raise AccountModError(status) uid = str(_info(login_name)['uid']) f = os.path.join(users_root_dir_, uid, uid + '.%s' % ext) try: extfile = _read_file(f) except Exception as reason: raise AccountModError(reason) for key in settings: extfile[key] = settings[key] try: _save_file(extfile, f) except Exception as reason: raise AccountModError(reason) return
def _check_permissions(file_info, info): """Check access permission on a file. _check_permissions(file_info, info) Returns a string: empty if access granted or a "denied access" page otherwise. """ import aux import manage_users gids = manage_users._info(info['login_name'])['gids'] denied_page = '' info['title'] = 'File unavailable' info['details'] = 'You no longer have access to this file.' info['status_button_1'] = """ <form action="../file_manager.py/process?start" method="post"> <input type="hidden" name="token" value="%s" /> <input type="submit" value="Files" /> </form> """ % info['token'] info['status_button_2'] = '' info['class'] = 'warning' if not file_info: denied_page = aux._fill_page(info['status_page_'], info) elif not info['uid'] == file_info['owner_uid'] and \ not file_info['local_share'] and \ not info['uid'] in file_info['uid_shares'] and \ not set(gids).intersection(set(file_info['gid_shares'])): denied_page = aux._fill_page(info['status_page_'], info) return denied_page
def _confirm(req, info): import logging import manage_kbasix import manage_users import aux logging.debug('Starting confirmation') info['title'] = aux._fill_str(info['confirmation_title'], info) info['status_button_1'] = '' info['status_button_2'] = '' if 'token' in req.form: token = req.form['token'] else: token = '' session = manage_kbasix._check_token(token, first_time=True) if not session: info['class'] = 'fail' info[ 'details'] = 'Failed to confirm account due to an invalid or stale token (probably the account confirmation deadline has passed)' logging.warn(info['details'] + ' [%s]' % token) return aux._fill_page(info['status_page_'], info) else: session['login_name'] = manage_users._info( session['uid'])['login_name'] info.update(session) manage_users._mod(info['login_name'], {'locked': False}) manage_kbasix._delete_token(info['uid']) profile = manage_kbasix._account_info(info['login_name'], 'profile') info.update(profile) info['class'] = 'success' info['details'] = aux._fill_str(info['successful_confirmation_blurb'], info) logging.info('Successfully confirmed account "%s"' % info['login_name']) return aux._fill_page(info['status_page_'], info)
def _initialize(info): import logging import manage_kbasix import manage_users import aux profile = manage_kbasix._account_info(info['login_name'], 'profile') if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: info['internal_disabled'] = '' else: info['internal_disabled'] = 'disabled' user = manage_users._info(info['login_name']) info['first_name'] = user['first_name'] info['last_name'] = user['last_name'] if user['auth_method'] == 'ldap': info['use_ldap'] = 'checked' else: info['use_ldap'] = '' info['ldap_options'] = '' for key in info['ldap_servers']: if user['auth_server'] == info['ldap_servers'][key]['server_']: info['ldap_servers'][key]['selected_'] = 'selected' info['ldap_options'] += info['ldap_servers'][key]['option_'] % info[ 'ldap_servers'][key] info['user_ldap_name'] = user['user_auth_name'] info.update(profile) logging.debug('Starting profile page (%s)' % info['login_name']) return aux._fill_page(info['profile_page_'], info)
def _initialize(info): import logging import manage_kbasix import manage_users import aux profile = manage_kbasix._account_info(info['login_name'], 'profile') if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: info['internal_disabled'] = '' else: info['internal_disabled'] = 'disabled' user = manage_users._info(info['login_name']) info['first_name'] = user['first_name'] info['last_name'] = user['last_name'] if user['auth_method'] == 'ldap': info['use_ldap'] = 'checked' else: info['use_ldap'] = '' info['ldap_options'] = '' for key in info['ldap_servers']: if user['auth_server'] == info['ldap_servers'][key]['server_']: info['ldap_servers'][key]['selected_'] = 'selected' info['ldap_options'] += info['ldap_servers'][key]['option_'] % info['ldap_servers'][key] info['user_ldap_name'] = user['user_auth_name'] info.update(profile) logging.debug('Starting profile page (%s)' % info['login_name']) return aux._fill_page(info['profile_page_'], info)
def _parse_shares(shares, shares_type, info): """Parse the string of user/group name shares into a list. Checks are made for deleted shares and missing users (prepended by a '-' and '*', respectively). (ids, err) = _parse_shares(shares, shares_type, info) Returns a tuple (list, str) with the uids/gids and an error string (if any). """ import logging import manage_users ids = [] err = '' # Note that the shares have already been forced to be ASCII-encoded in # '_update'. if shares_type == 'uid_shares': shares_type = 'account' shares_id = 'uid' # You cannot share with yourself names = list(set([i.strip().lower() for i in shares.split(',') if \ i.strip().lower() != info['login_name']])) else: # Groups are case-sensitive (since their names are not enforced by # manage_kbasix). shares_type = 'group' shares_id = 'gid' names = list(set([i.strip() for i in shares.split(',')])) logging.debug('Parsed shares (%s): %s' % (info['login_name'], names)) if names == ['']: return (ids, err) for i in names: try: # _get_shares will prepend the '-' if the share was deleted, # and use *.# for deleted users. if i[0] == '-': i = i[1:] if i[0] != '*': ids.append( manage_users._info(i, is_type=shares_type)[shares_id]) else: ids.append(int(i.split('.')[-1])) except: err += 'Ignoring unknown %s "%s"<br>' % (shares_type, i) logging.warn('Ignoring unknown id "%s" (%s)' % \ (i, info['login_name'])) return (sorted(ids), err)
def _finger(login_name): """Interactively retrieve information about a user account. info = _finger(login_name) The return type may vary, should only be used interactively. """ (OK, status) = _check_args(locals()) if not OK: raise FingerError(status) import pprint user_info = _info(login_name) if not user_info: return 'User "%s" not found.' % login_name uid = str(user_info['uid']) f = os.path.join(users_root_dir_, uid, uid + '.%s' % 'profile') try: return pprint.pprint(_read_file(f, lock=False)) except Exception as reason: raise FingerError(reason)
def _confirmation_email(req, info): """Send a confirmation email to complete registration. _confirmation_email(req, info) Returns nothing. """ import logging import os import manage_kbasix import manage_users import smtplib import aux from email.mime.text import MIMEText logging.debug('Sending confirmation email for "%s"' % \ info['login_name']) uid = manage_users._info(info['login_name'])['uid'] # We set 'ip_set=False' so that users can confirm their account # from any IP. token = manage_kbasix._create_token(req, uid, ip_set=False) www_path = os.path.dirname((req.subprocess_env['SCRIPT_NAME'])) confirm_urn = '/confirm.py/process?start&token=%s' % token info['confirm_url'] = req.construct_url(www_path + confirm_urn) msg = MIMEText(aux._fill_str(info['confirmation_notice'], info)) msg['Subject'] = aux._fill_str(info['email_subject'], info) msg['From'] = info['email_from_'] msg['To'] = info['user_email'] try: s = smtplib.SMTP(info['smtp_host_']) s.sendmail(info['email_from_'], [info['user_email']], \ msg.as_string()) s.quit() except Exception as reason: logging.error('Unable to send confirmation email because \ "%s" (%s)' % (reason, info['login_name'])) raise ConfirmationEmailError(reason) logging.debug('Confirmation email sent sucessfully for "%s" to "%s"' % \ (info['login_name'], info['user_email'])) return
def _account_info(login_name, ext): """Retrieve information about a user account. info = _account_info(login_name, ext) The 'ext' can be 'profile' or 'prefs' depending if a user's profile information or preferences are being accessed. Return is a dictionary (possibly an empty one). This function is suitable for function calls from other programs. """ (OK, status) = _check_args(locals()) if not OK: raise AccountInfoError(status) user_info = _info(login_name) if not user_info: return {} uid = str(user_info['uid']) f = os.path.join(users_root_dir_, uid, uid + '.%s' % ext) try: return _read_file(f, lock=False) except Exception as reason: raise AccountInfoError(reason)
def _login(req, info): """Try to login the user, or send an email token if the password is forgotten. _login(req, info) Returns a status page. """ if 'forgot_password' in req.form: try: return _send_token(req, info) except Exception as reason: raise SendTokenError(reason) import logging from mod_python import apache import manage_users import manage_kbasix import time import aux from mod_python import util logging.debug('Starting login process for "%s"' % info['login_name']) if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: allow_internal = True else: allow_internal = False password = req.form['user_password'].value # Note that '_authenticate' only returns the uid if 'is_usr' is # True, otherwise it'll return a keyword specifying the authentication # failure point. try: (is_usr, status) = manage_users._authenticate(info['login_name'], \ password) logging.info('Authentication for "%s" was "%s" with \ status/uid: %s' % (info['login_name'], is_usr, status)) except Exception as reason: raise LoginError(reason) blocked = False # We don't just show the reasons for authentication failures, but # instead use codes defined in 'defs.py' (the log is explicit in this # respect). if is_usr: uid = status locked = manage_users._info(info['login_name'])['locked'] if locked: blocked = True msg = info['reason_not_active_'] else: blocked = True msg = info['reason_auth_fail_'] if not blocked: auth_method = manage_users._info(info['login_name'])['auth_method'] # An empty 'auth_method' means the auth is internal. if not auth_method and not allow_internal: blocked = True msg = info['reason_not_allowed_'] elif info['login_name'] in info['banned_logins']: blocked = True msg = info['reason_banned_'] if not blocked: try: info['token'] = manage_kbasix._create_token(req, uid) info['user_name'] = \ manage_kbasix._account_info(info['login_name'], \ 'profile')['user_name'] info['access'] = 'all' info['class'] = 'information' info['main_header'] = aux._make_header(info) info['title'] = aux._fill_str(info['welcome_title'], info) info['details'] = aux._fill_str(info['welcome_blurb'], info) info['status_button_1'] = """ <form action="../%(referrer)s/process?start" method="post"> <input type="hidden" name="token" value="%(token)s"> <input type="submit" value="Continue" /> </form> """ % info info['status_button_2'] = '' manage_kbasix._account_mod(info['login_name'], \ 'profile', {'last_login': \ time.time()}) logging.info('Successful login from %s (%s)' % \ (req.get_remote_host(apache.REMOTE_NOLOOKUP), \ info['login_name'])) return aux._fill_page(info['status_page_'], info) except Exception as reason: raise LoginError(reason) else: info['class'] = 'fail' info['title'] = 'Login' info['details'] = aux._fill_str(msg, info) info['status_button_1'] = aux._go_back_button(req, token = '') info['status_button_2'] = '' logging.info('Failed login for "%s" because "%s"' % \ (info['login_name'], info['details'])) return aux._fill_page(info['status_page_'], info)
def _get_file_list(info): """Get the file listing. file_list = _get_file_list(info) Returns a string. """ # This function is way too big, it needs to be sensibly split up. import logging import os import fnmatch import time import aux import manage_users import manage_kbasix login_name = info['login_name'] logging.debug('Creating a file list (%s)' % login_name) prefs = manage_kbasix._account_info(login_name, 'prefs') user_dir = os.path.join(info['users_root_dir_'], str(info['uid'])) entries = {} count = {} gids = manage_users._info(login_name)['gids'] # Check for local- and GID-shared files. for subdir in ['local'] + [str(i) for i in gids]: subpath = os.path.join(info['shared_dir_'], subdir) if os.path.exists(subpath): # Some house cleaning: delete broken symlinks. for i in os.listdir(subpath): f = os.path.join(subpath, i) # We are lenient with file removals in shared directories # because another user might have beat us to it. if not os.path.exists(f): try: os.remove(f) logging.info('Deleted broken shared symlink "%s" \ (%s)' % (f, login_name)) except Exception as reason: logging.warn(reason) for i in fnmatch.filter(os.listdir(subpath), '*-file'): rel_path = os.path.relpath(subpath, user_dir) src = os.path.join(rel_path, i) dst = os.path.join(user_dir, i) # Shared entries which are deleted by the user (the # sharee) are actually hidden from view by adding them # to the 'hidden_gidloc_shared_files' list when the # sharee deletes them. # Once hidden, local/gid-shared files cannot be recovered # unless explicitly re-shared with that user via uid_shares. # Note that if the file exists (important if it's a uid # point share, which takes precedence) we leave it alone. if not os.path.exists(dst) and \ i not in \ prefs['file_manager']['hidden_gidloc_shared_files']: try: # The target may not exist, but if the link does, # delete it. if os.path.islink(dst + '-id'): os.remove(dst + '-id') os.symlink(src + '-id', dst + '-id') except: logging.error('Cannot link shared id file "%s" \ (%s)' % (src + '-id', login_name)) else: if os.path.islink(dst): os.remove(dst) os.symlink(src, dst) logging.debug('Added shared file "%s" -> "%s" \ (%s)' % (src, dst, login_name)) # Create the listing of the files/symlinks in the user's directory. for i in os.listdir(user_dir): f = os.path.join(user_dir, i) if not os.path.exists(f): try: os.remove(f) logging.info('Deleted broken user symlink "%s" (%s)' % \ (f, login_name)) except Exception as reason: logging.error(reason) for i in fnmatch.filter(os.listdir(user_dir), '*-id'): f = os.path.join(user_dir, i) # This should not normally happen. The magic [:-3] deletes '-id' # and leaves the content file name. if not os.path.exists(f[:-3]): try: os.remove(f) logging.error('Deleted orphan id file "%s" (%s)' % \ (f, login_name)) except Exception as reason: logging.error(reason) continue details = manage_users._read_file(f, lock=False) # A symlink implies the file is being shared with the user. if os.path.islink(f): details['shared_with_me'] = True else: details['shared_with_me'] = False for j in ['file_title', 'file_description']: if not details[j]: details[j] = info['empty_placeholder_'] details['file_date'] = \ time.strftime(info['file_manager_time_format_'], \ time.localtime(details['timestamp'])) # We need the token for the buttons. details['token'] = info['token'] sort_key = details[prefs['file_manager']['sort_criteria']] # We pad with zeros to obtain a natural sorting for entries with # the same non-string keys e.g. size or timestamp. Padding to 50 # seems safe, although this is admittedly a magic number # (timestamps have less than 20 digits, and files size are smaller # than that). Worst case scenario is that the sorting on huge # numeric sort keys (larger than 50 digits) will be wrong, but this # is unlikely to happen and has no other consequences. if not isinstance(sort_key, basestring): sort_key = '%r' % sort_key sort_key = sort_key.zfill(50) # We internally distinguish between identical entry values # (say, identical file names), but although lumped together # in their proper place within the overall list they are # not sub-sorted in any deliberate way. if sort_key not in entries: count[sort_key] = 1 entries[sort_key] = details else: count[sort_key] += 1 entries[sort_key + ' (%s)' % count[sort_key]] = details if prefs['file_manager']['condensed_view']: info['template'] = 'condensed_entry_template_' else: info['template'] = 'entry_template_' file_list = '' for key in sorted(entries, \ reverse=bool(prefs['file_manager']['reverse'])): entries[key]['shared_status'] = '' entries[key]['file_colour'] = info['my_file_colour_'] if entries[key]['shared_with_me']: # World shares are not symlinked with the individual accounts, # and entries which cannot be read any longer are deleted. if not entries[key]['local_share'] and \ not info['uid'] in entries[key]['uid_shares'] and not \ set(gids).intersection(set(entries[key]['gid_shares'])): shared_file = \ os.path.join(user_dir, entries[key]['file_tag']) try: os.remove(shared_file) os.remove(shared_file + '-id') except Exception as reason: logging.error(reason) continue # Don't show the shares made by no-longer-exsting users if # 'exusers_cannot_share' is True. elif not manage_users._info(entries[key]['owner_uid']) and \ info['exusers_cannot_share']: continue else: entries[key]['shared_status'] = """ <img src="%s" title="File shared with me by: %s" alt="[File shared with me by: %s]" /> """ % (info['shared_icon_with_me_'], entries[key]['owner'], \ entries[key]['owner']) entries[key]['file_colour'] = info['other_file_colour_'] if prefs['file_manager']['hide_shared'] and \ entries[key]['shared_with_me']: continue # Shared files can be copied internally (it's more efficient than # downloading and uploading again). entries[key]['copy_file'] = '' if entries[key]['shared_with_me']: entries[key]['copy_file_icon_'] = info['copy_file_icon_'] entries[key]['copy_file_form_style_'] = \ info['copy_file_form_style_'] entries[key]['copy_file'] = """ <form %(copy_file_form_style_)s action="../file_manager.py/process?action=copy_file" method="post"> <input type="hidden" name="token" value="%(token)s" /> <input type="hidden" name="file_tag" value="%(file_tag)s" /> <input title="Make a local copy" type="image" alt="Make a local copy" src="%(copy_file_icon_)s" /> </form> """ % entries[key] # Files that are shared (the user being the sharer) are tagged # in various ways to indicate this. else: if entries[key]['world_share']: entries[key]['shared_status'] += """ <img src="%s" title="File is shared with the world" alt="[File is shared with the world]" /> """ % info['shared_icon_by_me_to_world_'] if entries[key]['local_share']: entries[key]['shared_status'] += """ <img src="%s" title="File is shared with registered users" alt="[File is shared with registered users]" /> """ % info['shared_icon_by_me_locally_'] if entries[key]['gid_shares']: entries[key]['shared_status'] += """ <img src="%s" title="File is shared with selected groups" alt="[File is shared with selected groups]" /> """ % info['shared_icon_by_me_to_groups_'] if entries[key]['uid_shares']: entries[key]['shared_status'] += """ <img src="%s" title="File is shared with selected users" alt="[File is shared with selected users]" /> """ % info['shared_icon_by_me_selectively_'] # Convert the size to a string with the appropriate unit suffix e.g. # 'MB'. entries[key]['file_size_str'] = \ aux._bytes_string(entries[key]['file_size']) entries[key]['toggle_select'] = info['toggle_select'] # Limit the length of titles and descriptions. for i in ['description', 'title']: if info['max_chars_in_' + i]: max_char = len(entries[key]['file_' + i]) char_num = min(max_char, info['max_chars_in_' + i]) entries[key][i + '_blurb'] = \ entries[key]['file_' + i][:char_num] if max_char > char_num: entries[key][i + '_blurb'] += '...' else: entries[key][i + '_blurb'] = entries[key]['file_' + i] # The keyword filter. This is actually a very important # functionality, and should be split into its own function. # Furthermore, it is currently very weak, and should be drastically # improved. Right now is just filters on words in the file title # and description, ignoring punctuation. It is also # case-insensitive. if prefs['file_manager']['keywords']: import re # Use '[\W_]+' to eliminate underscores. See also: # # http://stackoverflow.com/questions/6631870/strip-non-alpha-numeric-characters-from-string-in-python-but-keeping-special-cha # # The 're.U' works on unicode strings. nonalnum = re.compile('[\W]+', re.U) keywords = set([i.lower() for i in \ prefs['file_manager']['keywords'].split()]) words = [nonalnum.sub('', i.lower()) for i in \ entries[key]['file_title'].split()] words += [nonalnum.sub('', i.lower()) for i in \ entries[key]['file_description'].split()] if keywords <= set(words): file_list += aux._fill_str(info[info['template']], \ entries[key]) continue file_list += aux._fill_str(info[info['template']], entries[key]) if not file_list: return info['no_files_found_'] else: return file_list
def _update(req, info): import logging import re import time import manage_users import manage_kbasix import aux if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: allow_internal = True else: allow_internal = False profile = {} account = {} account['first_name'] = req.form['first_name'].value account['last_name'] = req.form['last_name'].value profile['user_email'] = req.form['user_email'].value err = 0 info['details'] = '' if not re.match(info['valid_email_'], profile['user_email'], re.I): err += 1 info['details'] += 'Please input a valid email address.<br>' if 'user_password' in req.form: info['user_password'] = req.form['user_password'].value else: info['user_password'] = '' info['user_ldap_password'] = req.form['user_ldap_password'].value info['user_ldap_name'] = req.form['user_ldap_name'].value was_external = manage_users._info(info['login_name'])['auth_method'] if 'use_ldap' in req.form and info['user_ldap_password']: if not info['user_ldap_name']: err += 1 info['details'] += 'Please fill out all the LDAP credentials.<br>' else: from register import _check_ldap info['ldap_server'] = req.form['ldap_server'].value if info['ldap_server']: (err, info['details']) = _check_ldap(info, err) else: err += 1 info['details'] += 'Please specify the LDAP server.<br>' if not err: account['password'] = '******' account['auth_method'] = 'ldap' account['auth_server'] = info['ldap_server'] account['user_auth_name'] = info['user_ldap_name'] if not was_external: info[ 'details'] += 'Authentication is no longer internal.<br>' elif info['user_password'] and allow_internal: from register import _check_password info['user_password_check'] = req.form['user_password_check'].value (err, info['details']) = _check_password(info, err) if not err: account['auth_method'] = '' account['auth_server'] = account['user_auth_name'] = '' account['password'] = info['user_password'] if was_external: info['details'] += 'Authentication is now internal.<br>' else: info['details'] += 'Note: no authentication changes made.<br>' if not err: try: manage_kbasix._account_mod(info['login_name'], 'profile', profile) manage_users._mod(info['login_name'], account) except Exception as reason: raise UpdateError(reason) if err: info['class'] = 'fail' info['status_button_1'] = aux._go_back_button(req, info['token']) info['status_button_2'] = '' logging.debug('Failed profile update because "%s" (%s)' % \ (info['details'].replace('<br>',' '), info['login_name'])) else: info['class'] = 'success' info['details'] += aux._fill_str(info['successful_update_blurb'], info) if info['access'] == 'profile.py': info['status_button_1'] = """ <form action="../login.py/process?start" method="post"> <input type="submit" value="Login" /> </form><br> """ % info else: info['status_button_1'] = '' info['status_button_2'] = '' logging.debug('Successful profile update (%s)' % info['login_name']) info['title'] = 'User Profile' return aux._fill_page(info['status_page_'], info)
def _get_shares(file_info, info): """Convert UIDs/GIDs/boolean shares as stored in the file metadata into actual user names/group names/HTML 'checked' (or not). (users, groups, local, world) = _get_shares(file_info, info) Returns a (list, list, str, str) tuple. """ import logging import os import manage_kbasix import manage_users id_file = file_info['file_tag'] + '-id' users = [] for i in file_info['uid_shares']: user_info = manage_users._info(i) # If 'remove_exusers_shares' is False we'll try to keep # the shares of non-existent users (so that if they return # they can recover said shares). However, this doesn't work # well in practice and the metadata editor might wipe it out # during an update. Furthermore, the user might be confused # when seeing '*.7' in the list. Same for 'remove_exgroups_shares'. if not user_info: if not info['remove_exusers_shares']: users.append('*.%s' % i) else: # Users who have deleted the file being shared with them # show up with a '-' prefix in the shared user list. user_name = \ manage_kbasix._account_info(user_info['login_name'], \ 'profile')['user_name'] if not os.path.exists(os.path.join(info['users_root_dir_'], \ str(user_info['uid']), \ id_file)): users.append('-' + user_name) else: users.append(user_name) if users: users = ', '.join(users) else: users = '' groups = [] for i in file_info['gid_shares']: group_info = manage_users._info(i, is_type='group') if not group_info: if not info['remove_exgroups_shares']: groups.append('*.%s' % i) else: groups.append(group_info['group_name']) if groups: groups = ', '.join(groups) else: groups = '' # The booleans become 'checked' if True, '' otherwise. if file_info['local_share']: local = 'checked' else: local = '' if file_info['world_share']: world = 'checked' else: world = '' return (users, groups, local, world)
def _account_add(login_name, settings): """Add a KBasix user account. _account_add(login_name, settings) The 'settings' dictionary is stored in the user's profile. Its only requirement is to define the keys 'user_name' and 'login_name' (such that login_name = user_name.lower(), where login_name is the user login name handled by the KBasix user management module. Returns nothing. """ (OK, status) = _check_args(locals()) if not OK: raise AccountAddError(status) if 'user_name' not in settings: raise AccountAddError('Account settings must contain "user_name"') user_name = settings['user_name'] if login_name != settings['login_name']: raise AccountAddError('Inconsistent "login_name" ("%s" and "%s")', \ (login_name, settings['login_name'])) # This 'user_name' is the only user input saved directly on disk and # hence the extra security checks (which should never be triggered). # Although the length of the variable is set by 'login_name_max_length', # it's very hard to believe a user name will be over 100 characters # long, and furthermore such long names can cause issues with the # filename length limit on the file system (system-dependent, but # usually 255 characters). if not user_name.isalnum() or len(user_name) > 100: raise AccountAddError('Invalid "user_name": %s' % user_name) uid = str(_info(login_name)['uid']) # The 'world' subdirectory is created, and is in turned symlinked # from the overall 'www_dir_' which contains the world-exposed # user files. Note that the directory symlink name under 'www_dir_' # is the user's user name. Note that the user directories are # identified by uid. user_dir = os.path.join(users_root_dir_, uid) # This 'world_dir' (note the lack of underscore) is the physical # 'world' dir which lives under the user's directory within # 'users_root_dir_'. 'world_dir_' is the top-level http-accessible # directory (which lives within DocumentRoot) and within it the # the symlink 'world_link' is created (which uses user name as # as opposed to uid -- it is deleted when an account is deleted). world_dir = os.path.join(user_dir, 'world') world_link = os.path.join(www_dir_, user_name) rel_path = os.path.relpath(world_dir, www_dir_) profile = os.path.join(user_dir, uid + '.profile') preferences = os.path.join(user_dir, uid + '.prefs') try: if not os.path.isdir(user_dir): os.makedirs(user_dir) os.chmod(user_dir, 0700) if not os.path.isdir(world_dir): os.makedirs(world_dir) os.chmod(world_dir, 0700) if not os.path.islink(world_link): os.symlink(rel_path, world_link) except Exception as reason: raise AccountAddError(reason) try: _save_file({}, preferences, unlock=False) _save_file(settings, profile, unlock=False) except Exception as reason: raise AccountAddError(reason) return
def _is_session(req, required, holdover=False): """Checks the validity of a session. session = _is_session(req, required, holdover=False) If 'first_time' is True the token's timestamp is checked against 'account_confirmation_timeout'. The 'required' boolean sets whether a login is needed to access the page, and if so performs a redirect to the login module 'login.py'. A bad token results in an empty dictionary returning, otherwise the following dictionary is returned: {'token': the token itself, 'uid': the numeric uid (int), 'start': the time when the token was created (float), 'session': the unique session id, 'client_ip': the client IP (or '0.0.0.256' if not set), 'access': name of the accessible module or 'all'} """ (OK, status) = _check_args(locals()) if not OK: raise IsSessionError(status) from mod_python import apache, util # Using None actually puts 'None' as the token string on the page, # which is undesirable as it checks out True. no_session = {'token': '', 'login_name': '', 'uid': '', \ 'start': '', 'session': '', 'client_ip': '', \ 'access': ''} # We do nothing over unencrypted connections if not req.is_https(): return no_session referrer = os.path.basename(req.canonical_filename) # A login redirect will not have 'file_tag', so if the user # is logged out while in the metadata editor and the logs in # the redirect will be to the file manager instead. if referrer == 'metaeditor.py': referrer = 'file_manager.py' if 'token' not in req.form: token = '' else: token = req.form['token'] session = _check_token(token) if session: # The following exception may be triggered is a user is deleted # mid-session. try: session['login_name'] = _info(session['uid'])['login_name'] except: session = {} # Terminate session if the client IP changes (and ip_set is True) if session and per_client_ip_token: if session['client_ip'] == '0.0.0.256': pass elif session['client_ip'] != \ req.get_remote_host(apache.REMOTE_NOLOOKUP): session = {} if session and session['access'] not in ['all', referrer]: session = {} if session and per_request_token: # For extra security we create a token per request, except if we # explicitly require a holdover (needed when client-based windows # are generated e.g. a download query window). if holdover: return session else: token = _create_token(req, session['uid']) session = _check_token(token) if session: session['login_name'] = _info(session['uid'])['login_name'] if not session and required: util.redirect(req, '../login.py/process?start&referrer=%s' % \ referrer) if session: return session else: return no_session
def _update(req, info): import logging import re import time import manage_users import manage_kbasix import aux if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: allow_internal = True else: allow_internal = False profile = {} account = {} account['first_name'] = req.form['first_name'].value account['last_name'] = req.form['last_name'].value profile['user_email'] = req.form['user_email'].value err = 0 info['details'] = '' if not re.match(info['valid_email_'], profile['user_email'], re.I): err += 1 info['details'] += 'Please input a valid email address.<br>' if 'user_password' in req.form: info['user_password'] = req.form['user_password'].value else: info['user_password'] = '' info['user_ldap_password'] = req.form['user_ldap_password'].value info['user_ldap_name'] = req.form['user_ldap_name'].value was_external = manage_users._info(info['login_name'])['auth_method'] if 'use_ldap' in req.form and info['user_ldap_password']: if not info['user_ldap_name']: err += 1 info['details'] += 'Please fill out all the LDAP credentials.<br>' else: from register import _check_ldap info['ldap_server'] = req.form['ldap_server'].value if info['ldap_server']: (err, info['details']) = _check_ldap(info, err) else: err += 1 info['details'] += 'Please specify the LDAP server.<br>' if not err: account['password'] = '******' account['auth_method'] = 'ldap' account['auth_server'] = info['ldap_server'] account['user_auth_name'] = info['user_ldap_name'] if not was_external: info['details'] += 'Authentication is no longer internal.<br>' elif info['user_password'] and allow_internal: from register import _check_password info['user_password_check'] = req.form['user_password_check'].value (err, info['details']) = _check_password(info, err) if not err: account['auth_method'] = '' account['auth_server'] = account['user_auth_name'] = '' account['password'] = info['user_password'] if was_external: info['details'] += 'Authentication is now internal.<br>' else: info['details'] += 'Note: no authentication changes made.<br>' if not err: try: manage_kbasix._account_mod(info['login_name'], 'profile', profile) manage_users._mod(info['login_name'], account) except Exception as reason: raise UpdateError(reason) if err: info['class'] = 'fail' info['status_button_1'] = aux._go_back_button(req, info['token']) info['status_button_2'] = '' logging.debug('Failed profile update because "%s" (%s)' % \ (info['details'].replace('<br>',' '), info['login_name'])) else: info['class'] = 'success' info['details'] += aux._fill_str(info['successful_update_blurb'], info) if info['access'] == 'profile.py': info['status_button_1'] = """ <form action="../login.py/process?start" method="post"> <input type="submit" value="Login" /> </form><br> """ % info else: info['status_button_1'] = '' info['status_button_2'] = '' logging.debug('Successful profile update (%s)' % info['login_name']) info['title'] = 'User Profile' return aux._fill_page(info['status_page_'], info)