Source code for ftp.api

#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# Interpreter version: python 2.7
#
# Imports =====================================================================
"""
ProFTPD_ wrapped used to manage users of the FTP server.

This module controls the ``ftpd.passwd`` (:attr:`LOGIN_FILE
<ftp.settings.LOGIN_FILE>`), creates/removes users directory and so on.

.. _ftpasswd: http://www.proftpd.org/docs/contrib/ftpasswd.html
.. _ProFTPD: http://www.proftpd.org

Warning:
    This API supposes, that it has permissions to read/write to `ProFTPD`
    configuration directory and to `root` directory for users.

Note:
    You don't have to set the permissions and everything manually, there is
    script called :mod:`.initializer`, which can do it for you automatically.
"""
import re
import os
import os.path
import shutil
from functools import wraps

import sh

import settings
import passwd_reader


# Functions & objects =========================================================
[docs]def require_root(fn): """ Decorator to make sure, that user is root. """ @wraps(fn) def xex(*args, **kwargs): assert os.geteuid() == 0, \ "You have to be root to run function '%s'." % fn.__name__ return fn(*args, **kwargs) return xex
@require_root
[docs]def reload_configuration(): """ Send signal to the proftpd daemon to reload configuration. """ sh.killall("-HUP", "proftpd", _ok_code=[0, 1])
[docs]def recursive_chmod(path, mode=0755): """ Recursively change ``mode`` for given ``path``. Same as ``chmod -R mode``. Args: path (str): Path of the directory/file. mode (octal int, default 0755): New mode of the file. Warning: Don't forget to add ``0`` at the beginning of the numbers of `mode`, or `Unspeakable hOrRoRs` will be awaken from their unholy sleep outside of the reality and they WILL eat your soul (and your files). """ passwd_reader.set_permissions(path, mode=mode) if os.path.isfile(path): return # recursively change mode of all subdirectories for root, dirs, files in os.walk(path): for fn in files + dirs: passwd_reader.set_permissions(os.path.join(root, fn), mode=mode)
def _is_valid_username(username): """ Check if username consist from characters "a-zA-Z0-9._-". Args: username (str): User's name. """ return re.search(r"^[a-zA-Z0-9\.\_\-]*$", username)
[docs]def create_lock_file(path): """ Create lock file filled with :attr:`LOCK_FILE_CONTENT <ftp.settings.LOCK_FILE_CONTENT>`. Args: path (str): Path to the lock file. Made from users home directory and :attr:`LOCK_FILENAME <ftp.settings.LOCK_FILENAME>`. """ with open(path, "w") as f: f.write(settings.LOCK_FILE_CONTENT) passwd_reader.set_permissions(path, gid=settings.PROFTPD_USERS_GID)
@require_root
[docs]def add_user(username, password): """ Adds record to passwd-like file for ProFTPD, creates home directory and sets permissions for important files. Args: username (str): User's name. password (str): User's password. """ assert _is_valid_username(username), \ "Invalid format of username '%s'!" % username assert username not in passwd_reader.load_users(), \ "User '%s' is already registered!" % username assert password, "Password is reqired!" # add new user to the proftpd's passwd file home_dir = settings.DATA_PATH + username sh.ftpasswd( passwd=True, # passwd file, not group file name=username, home=home_dir, # chroot in DATA_PATH shell="/bin/false", uid=settings.PROFTPD_USERS_GID, # TODO: parse dynamically? gid=settings.PROFTPD_USERS_GID, stdin=True, # tell ftpasswd to read password from stdin file=settings.LOGIN_FILE, _in=password ) # create home dir if not exists if not os.path.exists(home_dir): os.makedirs(home_dir, 0775) # I am using PROFTPD_USERS_GID (2000) for all our users - this GID # shouldn't be used by other than FTP users! passwd_reader.set_permissions(home_dir, gid=settings.PROFTPD_USERS_GID) passwd_reader.set_permissions(settings.LOGIN_FILE, mode=0600) create_lock_file(home_dir + "/" + settings.LOCK_FILENAME) reload_configuration()
@require_root
[docs]def remove_user(username): """ Remove user, his home directory and so on.. Args: username (str): User's name. """ users = passwd_reader.load_users() assert username in users, "Username '%s' not found!" % username # remove user from passwd file del users[username] passwd_reader.save_users(users) # remove home directory home_dir = settings.DATA_PATH + username if os.path.exists(home_dir): shutil.rmtree(home_dir) reload_configuration()
@require_root
[docs]def change_password(username, new_password): """ Change password for given `username`. Args: username (str): User's name. new_password (str): User's new password. """ assert username in passwd_reader.load_users(),\ "Username '%s' not found!" % username sh.ftpasswd( "--change-password", passwd=True, # passwd file, not group file name=username, stdin=True, # tell ftpasswd to read password from stdin file=settings.LOGIN_FILE, _in=new_password ) reload_configuration()
@require_root
[docs]def list_users(): """ List all registered users, which are stored in :attr:`LOGIN_FILE <ftp.settings.LOGIN_FILE>`. Returns: list: of str usernames. """ return map(lambda (key, val): key, passwd_reader.load_users().items())