├── README.md ├── config ├── __init__.py ├── config.py ├── policies └── users ├── delegate-client.py ├── delegate-server.py └── lib ├── __init__.py ├── auth.py ├── config_loader.py ├── epoll.py ├── keymanager.py ├── logger.py ├── module.py ├── policy.py ├── process.py ├── request.py ├── request_queue.py ├── scripts.py ├── server.py └── sockets.py /README.md: -------------------------------------------------------------------------------- 1 | # Delegate 2 | 3 | ## Структура проекта: 4 | ### delegate-client.py 5 | Клиент, с помощью которого можно посылать запросы серверу. 6 | 7 | ### delegate-server.py 8 | Точка входа delegate сервера. 9 | 10 | ### config/ 11 | директория с конфигом delegate сервера. 12 | 13 | #### config/config.py 14 | 15 | ##### config 16 | config = { 17 | 'serve': '0.0.0.0', # на каком адресе слушаем порт 18 | 'port': 2390, # какой порт слушаем 19 | 'verbosity': 1, # вербозити движка 20 | 'salt1': b"ahThiodai0ohG1phokoo", # соль сервера 21 | 'salt2': b"Aej1ohv8Naish5Siec3U", # соль клиентов 22 | 'path_to_users': 'config/users', # путь до файла с пользователями 23 | 'path_to_policies': 'config/policies', # путь до файла с политиками 24 | 'limit': { # секция лимитов одновременных запусков скриптов из очередей 25 | 'default': 10, # настройка для дефолтной очереди (когда не указана очередь у скрипта) 26 | 'date': 2, # настройка для очереди с названием date 27 | } 28 | } 29 | Вторая секция конфига -- скрипты у данного delegate сервера 30 | ##### scripts 31 | scripts = { 32 | "test": { # название скрипта 33 | "cmd_line": "/bin/sleep", # исполняемый файл 34 | "need_arguments": True, # может ли пользователь передавать скрипту аргументы 35 | "default_arguments": ["5"], # дефолтный аргумент, который будет передан в любом случае 36 | }, 37 | "test_date": { 38 | "cmd_line": "/bin/date", 39 | "need_arguments": False, 40 | "lock": "date", # в какую очередь исполнения добавляется скрипт 41 | "default_arguments": [], 42 | }, 43 | } 44 | 45 | 46 | #### config/users 47 | Этот файл состоит из двух частей: 48 | -- пользователи 49 | -- группы 50 | Пользователи характеризуются именем и pre-shared ключом, переданным по безопасному каналу. 51 | 52 | username:pre-shared-key 53 | username2:pre-shared-key-2 54 | 55 | Группы характеризуются названием и списком пользователей из списка, объявленного выше и начинаются с ключевого слова group. 56 | 57 | group groupname : username username2 58 | *ВНИМАНИЕ:* Хорошим тоном является не перемешивать список пользователей с группами, т.к. в группу нельзя добавить еще не существующего пользователя, а конфиг парсится за 1 проход. 59 | 60 | #### config/policies 61 | 62 | Это файл политик. Изначально считается, что всем все запрещено и все политики имеют разрешительный характер. 63 | Политики бывают двух видов: 64 | -- Политика для пользователя начинается с 65 | 66 | -u username 67 | -- Политика для группы пользователей начинается с 68 | 69 | -g groupname 70 | Не бывает смешанных политик, делайте их сами, вручную. 71 | 72 | Полная строка политики состоит из трех частей: 73 | ##### На кого распространяется 74 | 75 | -u username 76 | -g groupname 77 | ##### Опциональная часть параметров политики 78 | 79 | -p param_name 80 | Параметров у одной политики может быть сколько угодно или не быть вообще. 81 | На данный момент есть 2 возможных параметра для политик: 82 | 83 | ALLOW_ARGUMENTS: разрешить пользователю передавать аргументы. Без этого параметра все аргументы, кроме дефолтных из config.py, игнорируются. 84 | PROVIDE_USERNAME: запустить проставив в env DELEGATE_USERNAME 85 | ##### Название скрипта (из config.py) 86 | 87 | scriptname 88 | 89 | ##### В итоге получаем файл политик вида: 90 | 91 | -u username -p param1 -p param2 script1 92 | -g groupname script2 93 | -g groupname1 -p param1 script1 94 | 95 | ### lib 96 | Подкапотное пространство delegate сервера. Без должной необходимости не ломайте там ничего. 97 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VKCOM/Delegate/70a109bff87fdc6abbeeba8dd3739e30c8fa2a2a/config/__init__.py -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | __author__ = 'VK OPS CREW ' 2 | 3 | config = { 4 | 'serve': '0.0.0.0', 5 | 'port': 2390, 6 | 'verbosity': 1, 7 | 'salt1': b"ahThiodai0ohG1phokoo", 8 | 'salt2': b"Aej1ohv8Naish5Siec3U", 9 | 'path_to_users': 'config/users', 10 | 'path_to_policies': 'config/policies', 11 | 'limit': { 12 | 'default': 10, 13 | 'date': 2 14 | } 15 | } 16 | 17 | scripts = { 18 | "test2": { 19 | "cmd_line": "/bin/sleep", 20 | "need_arguments": True, 21 | "default_arguments": ["5"], 22 | }, 23 | "test_args": { 24 | "cmd_line": "/bin/echo", 25 | "need_arguments": True, 26 | "default_arguments": ["5"], 27 | }, 28 | "test": { 29 | "cmd_line": "/bin/date", 30 | "need_arguments": False, 31 | "lock": "date", 32 | "default_arguments": [], 33 | }, 34 | "test_date2": { 35 | "cmd_line": "/bin/date", 36 | "need_arguments": False, 37 | "lock": None, 38 | "default_arguments": ["+%s"], 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /config/policies: -------------------------------------------------------------------------------- 1 | -u user1 test 2 | -u user1 test2 3 | -------------------------------------------------------------------------------- /config/users: -------------------------------------------------------------------------------- 1 | user1:asjdfoasdfasdfasdfgasdfasfasdfasd 2 | -------------------------------------------------------------------------------- /delegate-client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import hashlib 3 | import socket 4 | import sys 5 | import time 6 | import os 7 | from os.path import expanduser 8 | import argparse 9 | 10 | __author__ = 'VK OPS CREW ' 11 | salt2 = b"Aej1ohv8Naish5Siec3U" 12 | 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument( 15 | "target", 16 | help="host and port of delegate server you want to connect", 17 | metavar="host:port" 18 | ) 19 | parser.add_argument( 20 | "script", 21 | help="name of the script you want to launch" 22 | ) 23 | parser.add_argument( 24 | "-i", 25 | help="override default path to passfile", 26 | metavar="path/to/passfile" 27 | ) 28 | parser.add_argument( 29 | "params", 30 | help="parameters to script if applicable", 31 | metavar="params", 32 | nargs="*" 33 | ) 34 | args = parser.parse_args() 35 | 36 | if args.i: 37 | path_to_passfile = args.i 38 | else: 39 | path_to_passfile = "~/.delegate/passfile" 40 | 41 | path_to_passfile = expanduser(path_to_passfile) 42 | if not os.path.isfile(path_to_passfile): 43 | print("Can't find passfile: %s" % path_to_passfile) 44 | sys.exit(1) 45 | 46 | key_id, key_value = \ 47 | open(path_to_passfile).readline().strip().encode().split(b":") 48 | 49 | target = args.target 50 | host, port = target.split(':', 1) 51 | port = int(port) 52 | 53 | server_buffer = b'' 54 | server = socket.create_connection((host, port)) 55 | 56 | 57 | def query(data=None): 58 | global server, server_buffer 59 | if data is not None: 60 | r = 0 61 | data += b'\n' 62 | while r < len(data): 63 | r += server.send(data[r:]) 64 | if b'\n' not in server_buffer: 65 | while True: 66 | data = server.recv(2048) 67 | if not len(data): 68 | raise Exception('server has gone away') 69 | server_buffer += data 70 | if b'\n' in data: 71 | break 72 | result1, server_buffer = server_buffer.split(b'\n', 1) 73 | return result1 74 | 75 | try: 76 | hello = query(b'hello') 77 | result, key = hello.split() 78 | assert result == b'hello' 79 | 80 | command = [args.script.encode()] 81 | if args.params: 82 | for i in args.params: 83 | command.append(i.encode()) 84 | 85 | command_hash = hashlib.sha256( 86 | key_value + b':' + salt2 + b':' + key + b':' + b'%'.join(command) 87 | ).hexdigest().encode("ascii") 88 | started = query( 89 | b'run ' + key_id + b' ' + command_hash + b' ' + b' '.join(command) 90 | ) 91 | queued = None 92 | while started.startswith(b'queued:'): 93 | new_queued = int(started.split()[1]) 94 | if queued != new_queued: 95 | queued = new_queued 96 | print('\rqueue size: %d' % queued, end='') 97 | started = query(b'queue') 98 | time.sleep(0.1) 99 | if started != b'started': 100 | print("\rrun fail: %s" % started.decode('iso8859-1'), file=sys.stderr) 101 | sys.exit(1) 102 | 103 | print("\r[request send]") 104 | while True: 105 | log = query() 106 | if log == b'FINISH': 107 | break 108 | if log.startswith(b'queued:'): 109 | continue 110 | if log.startswith(b'LOG: '): 111 | log = log[5:] 112 | if log.startswith(b'stdout: '): 113 | log = log[8:] 114 | print(log.decode("utf-8")) 115 | continue 116 | elif log.startswith(b'stderr: '): 117 | log = log[8:] 118 | print(log.decode("utf-8"), file=sys.stderr) 119 | continue 120 | print("UNKNOWN: " + log.decode("utf-8")) 121 | except Exception as e: 122 | print(e) 123 | sys.exit(1) 124 | 125 | -------------------------------------------------------------------------------- /delegate-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | from config import config 5 | 6 | __author__ = 'VK OPS CREW ' 7 | 8 | config = config.config 9 | if not isinstance(config, dict): 10 | print("Can't open config") 11 | sys.exit(1) 12 | 13 | salt1 = config["salt1"] 14 | salt2 = config["salt2"] 15 | 16 | if __name__ == "__main__": 17 | from lib.logger import Logger 18 | from lib.keymanager import KeyManager 19 | from lib.policy import PolicyManager 20 | from lib.config_loader import ConfigLoader 21 | from lib.auth import Connector 22 | from lib.epoll import Epoll 23 | from lib.request_queue import RequestQueue 24 | from lib.server import Server 25 | from lib.sockets import ServerSocket 26 | from lib.process import Pool 27 | 28 | logger = Logger(outfile=config.get("log-file", None), verbosity=config["verbosity"]) 29 | keys = KeyManager(logger) 30 | policy = PolicyManager(keys, logger) 31 | loader = ConfigLoader( 32 | logger, 33 | config["path_to_users"], 34 | config["path_to_policies"], 35 | policy, 36 | keys 37 | ) 38 | res = loader.read() 39 | if not res: 40 | logger("Failed to read config. Exiting", "E") 41 | else: 42 | with Server( 43 | logger, keys, policy, 44 | config=config, 45 | queuer=RequestQueue 46 | ) as server: 47 | epoll = Epoll(server) 48 | process_pool = Pool(server) 49 | server_socket = ServerSocket(server, Connector) 50 | logger("Server started") 51 | server.run() 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VKCOM/Delegate/70a109bff87fdc6abbeeba8dd3739e30c8fa2a2a/lib/__init__.py -------------------------------------------------------------------------------- /lib/auth.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from config import config 3 | from lib.module import Module 4 | from lib.request import Request 5 | 6 | __author__ = 'VK OPS CREW ' 7 | 8 | connection_id = 0 9 | with open('/dev/random', 'rb') as f: 10 | salt_random = f.read(32) 11 | cfg = config.config 12 | 13 | 14 | class Connector(Module): 15 | def __init__(self, server1, socket): 16 | super(Connector, self).__init__(server1) 17 | global connection_id # , salt_random 18 | self.__id = connection_id 19 | self.__salt_random = salt_random 20 | with open('/dev/urandom', 'rb') as f1: 21 | self.__salt_random += f1.read(32) 22 | connection_id += 1 23 | self.__socket = socket 24 | self.__local_id = 0 25 | self.__hash = None 26 | self.__request = None 27 | self.__commands = { 28 | b'test': self.__cmd_test, 29 | b'hello': self.__cmd_hello 30 | } 31 | self.salt1 = cfg["salt1"] 32 | self.salt2 = cfg["salt2"] 33 | 34 | def __run(self, command_key, command_hash, command_run): 35 | self._log("__run") 36 | user_key = self._server.keys.get_user_key(command_key) 37 | if user_key is None: 38 | self._log('user not found: %s' % command_key.decode('ascii')) 39 | return self.__socket.write(b'Unauthorized\n') 40 | real_key = user_key 41 | real_hash = hashlib.sha256( 42 | b':'.join([ 43 | real_key, self.salt2, self.__hash, b'%'.join(command_run) 44 | ]) 45 | ).hexdigest().encode("ascii") 46 | self.__hash = None 47 | if command_hash != real_hash: 48 | self._log('hash mismatch: %s vs %s' % (command_hash, real_hash)) 49 | return self.__socket.write(b'Unauthorized\n') 50 | 51 | self.__request = Request( 52 | command_key, command_run[0], command_run[1:], self 53 | ) 54 | 55 | self._server.enqueue(self.__request) 56 | self.__commands[b'queue'] = self.__cmd_queue 57 | 58 | # commands 59 | def __cmd_test(self, arguments): 60 | self.__socket.write(b"test ok\n") 61 | 62 | def __cmd_hello(self, arguments): 63 | self.__hash = hashlib.sha256( 64 | self.salt1 + self.__salt_random + ( 65 | ":%d_%d" % (self.__id, self.__local_id) 66 | ).encode("ascii") 67 | ).hexdigest().encode("ascii") 68 | self.__local_id += 1 69 | self.__socket.write(b"hello " + self.__hash + b"\n") 70 | self.__commands[b'run'] = self.__cmd_run 71 | 72 | def __cmd_run(self, arguments): 73 | self._log("__cmd_run") 74 | if len(arguments) < 3 or self.__hash is None: 75 | return self.__socket.write(b'run failed') 76 | self.__run(arguments[0], arguments[1], arguments[2:]) 77 | 78 | def __cmd_queue(self, arguments): 79 | if self.__request is None: 80 | return self.write_error(b'no active request') 81 | self.write_queue(self.__request.index - self.__request.queue.done) 82 | 83 | # responses 84 | def write_log(self, message): 85 | self.__socket.write(b'LOG: ' + message + b'\n') 86 | 87 | def write_finish(self): 88 | self.__socket.write(b'FINISH\n') 89 | 90 | def write_start(self): 91 | self.__socket.write(b'started\n') 92 | 93 | def write_error(self, message): 94 | self.__socket.write(b'error: ' + message + b'\n') 95 | 96 | def write_queue(self, index): 97 | self.__socket.write(('queued: %d\n' % index).encode('ascii')) 98 | 99 | # caller 100 | def __call__(self, command): 101 | command = command.split() 102 | if len(command) == 0: 103 | return 104 | try: 105 | routine = self.__commands[command[0]] 106 | except KeyError: 107 | self.__socket.write(b"unknown command: " + command[0] + b"\n") 108 | routine = lambda *x: None 109 | return routine(command[1:]) 110 | -------------------------------------------------------------------------------- /lib/config_loader.py: -------------------------------------------------------------------------------- 1 | from lib.policy import Policy 2 | from config import config 3 | 4 | __author__ = 'VK OPS CREW ' 5 | cfg = config.config 6 | 7 | 8 | class ConfigLoader: 9 | def __init__(self, log, users_file, policies_file, policy_manager, key_manager): 10 | self.log = log 11 | self.users_file = users_file 12 | self.policies_file = policies_file 13 | self.policy_manager = policy_manager 14 | self.key_manager = key_manager 15 | 16 | def read(self): 17 | try: 18 | users_fd = open(self.users_file, "r") 19 | policies_fd = open(self.policies_file, "r") 20 | except IOError as e: 21 | self.log(e, "E") 22 | return False 23 | for line in users_fd.readlines(): 24 | if line.strip().encode() == b"": 25 | continue # empty line 26 | l = line.strip().encode().split(b"#")[0].strip().split(b":") 27 | # TODO: move this check somewhere else 28 | if len(l) > 2: 29 | self.log("More than one ':' while parsing users file?", "E") 30 | return False 31 | if b"group" in l[0][0:5]: 32 | self.key_manager.add_group(l[0].split(b" ")[1]) 33 | for u in l[1].strip().split(b" "): 34 | self.key_manager.add_group_member(u, l[0].split(b" ")[1]) 35 | else: 36 | self.key_manager.add_user(l[0].strip(), l[1].strip()) 37 | users_fd.close() 38 | 39 | cnt = 0 40 | for line in policies_fd.readlines(): 41 | if line.strip().encode() == b"": 42 | continue # empty line 43 | cnt += 1 44 | tokens = line.strip().encode().split(b"#")[0].split(b" ") 45 | policy = Policy() 46 | policy.parameters = [] 47 | prev_token = None 48 | for token in tokens: 49 | if prev_token == b'-u': 50 | policy.user = token 51 | elif prev_token == b'-g': 52 | policy.group = token 53 | elif prev_token == b'-p': 54 | policy.parameters.append(token) 55 | prev_token = token 56 | policy.script = tokens[-1] 57 | if not self.policy_manager.add_policy(policy): 58 | self.log(" File %s line %d" % (cfg["path_to_policies"], cnt), "E") 59 | return False 60 | policies_fd.close() 61 | return True 62 | -------------------------------------------------------------------------------- /lib/epoll.py: -------------------------------------------------------------------------------- 1 | import select 2 | from lib.module import Module 3 | 4 | __author__ = 'VK OPS CREW ' 5 | 6 | 7 | class Epoll(Module): 8 | def __init__(self, server1): 9 | super().__init__(server1) 10 | self.__epoll = select.epoll() 11 | self.__callback = {} 12 | self.__timeout = 0 13 | self.__default_timeout = 0.5 14 | self._server.epoll = self 15 | self._server.action_add(self._continue(self.poll, ())) 16 | self._server.action_sleep_add(self._continue(self.sleep, ())) 17 | 18 | def sleep(self): 19 | self.__timeout = self.__default_timeout 20 | 21 | def register(self, handler, callback): 22 | fileno = handler.fileno() 23 | assert fileno not in self.__callback 24 | self.__callback[fileno] = callback 25 | self.__epoll.register(handler, 26 | select.EPOLLIN | select.EPOLLOUT | select.EPOLLERR | select.EPOLLHUP | select.EPOLLET) 27 | 28 | def unregister(self, handler): 29 | fileno = handler.fileno() 30 | self.__epoll.unregister(handler) 31 | del self.__callback[fileno] 32 | 33 | def poll(self): 34 | self._log("poll from epoll#%d" % self.__epoll.fileno(), verbosity=5) 35 | try: 36 | for fileno, events in self.__epoll.poll(timeout=self.__timeout): 37 | self.__timeout = 0 38 | self._server.wake() 39 | self._log("event from epoll#%d for #%d:%d" % (self.__epoll.fileno(), fileno, events), verbosity=4) 40 | 41 | yield self._continue(self.__callback[fileno], (events,)) 42 | except IOError: 43 | # TODO: check if EINTR, exit if not 44 | pass 45 | -------------------------------------------------------------------------------- /lib/keymanager.py: -------------------------------------------------------------------------------- 1 | __author__ = 'VK OPS CREW ' 2 | 3 | 4 | class KeyManager: 5 | def __init__(self, logger): 6 | self.log = logger 7 | self.__users__ = dict() 8 | self.__groups__ = dict() 9 | 10 | def add_user(self, name, secret): 11 | if name in self.__users__: 12 | self.log("Already has user %s" % name, "W") 13 | return False 14 | else: 15 | self.log("Added new user %s" % name, "L", 2) 16 | self.__users__[name] = {"key": secret} 17 | return True 18 | 19 | def del_user(self, name): 20 | if name in self.__users__: 21 | if "groups" in self.__users__[name]: 22 | for i in self.__users__[name]["groups"]: 23 | self.__groups__[i].remove(name) 24 | self.__users__.__delitem__(name) 25 | self.log("Removed key for %s" % name) 26 | else: 27 | self.log("Can't remove non-existing user %s" % name, "E") 28 | 29 | def get_user_key(self, name): 30 | if name in self.__users__: 31 | self.log("Requested key for %s" % name, "L", 3) 32 | return self.__users__[name]["key"] 33 | else: 34 | self.log("Requested key for non-existing %s" % name, "E") 35 | return None 36 | 37 | def get_user_groups(self, name): 38 | if name in self.__users__: 39 | self.log("Requested groups for %s" % name, "L", 3) 40 | if "groups" in self.__users__[name]: 41 | return self.__users__[name]["groups"] 42 | else: 43 | return None 44 | 45 | def get_users(self): 46 | return [str(x) for x in self.__users__.keys()] 47 | 48 | def add_group(self, name): 49 | if name in self.__groups__: 50 | self.log("Already has a group %s" % name, "E") 51 | return False 52 | else: 53 | self.__groups__[name] = list() 54 | return True 55 | 56 | def add_group_member(self, username, groupname): 57 | if username in self.__users__: 58 | if groupname in self.__groups__: 59 | self.__groups__[groupname].append(username) 60 | if "groups" not in self.__users__[username]: 61 | self.__users__[username]["groups"] = list() 62 | self.__users__[username]["groups"].append(groupname) 63 | self.log("Added %s to %s" % (username, groupname), "L", 2) 64 | return True 65 | else: 66 | self.log("Has no group %s" % groupname, "E") 67 | return False 68 | else: 69 | self.log("Has no user %s" % username, "E") 70 | return False 71 | 72 | def remove_group_member(self, username, groupname): 73 | if username in self.__users__: 74 | if groupname in self.__groups__: 75 | if username in self.__groups__[groupname]: 76 | self.log("Removing %s from %s" % (username, groupname), "N", 2) 77 | self.__users__[username]["groups"].remove(groupname) 78 | self.__groups__[groupname].remove(username) 79 | return True 80 | else: 81 | self.log("Can't remove non-member %s from %s" % (username, groupname), "E") 82 | else: 83 | self.log("Can't remove from non-existing group %s" % groupname, "E") 84 | else: 85 | self.log("Can't remove non-existing user %s" % username, "E") 86 | return False 87 | 88 | def get_group_members(self, groupname): 89 | if groupname in self.__groups__: 90 | self.log("Requested group members for %s" % groupname, "N", 3) 91 | return self.__groups__[groupname] 92 | else: 93 | self.log("Requested group members for non-existing %s" % groupname, "E") 94 | return None 95 | 96 | def has_group(self, groupname): 97 | return groupname in self.__groups__ 98 | 99 | def has_user(self, username): 100 | return username in self.__users__ 101 | -------------------------------------------------------------------------------- /lib/logger.py: -------------------------------------------------------------------------------- 1 | __author__ = 'VK OPS CREW ' 2 | import sys 3 | import datetime 4 | 5 | 6 | class Logger: 7 | 8 | def __init__(self, outfile=None, verbosity=0): 9 | self.__name = outfile 10 | self.writer = sys.stderr if self.__name is None else open(self.__name, "a") 11 | self.__verbosity = verbosity 12 | 13 | def __call__(self, message, type_="L", verbosity=0): 14 | if self.__verbosity < verbosity: 15 | return 16 | result_string = "[%s]:%s:%s\n" % (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f %Z"), type_, message) 17 | self.writer.write(result_string) 18 | self.writer.flush() 19 | 20 | def log(self, message, type_="L", verbosity=0): 21 | self.__call__(message, type_, verbosity) 22 | 23 | def reopen(self): 24 | self.writer.close() 25 | self.writer = sys.stderr if self.__name is None else open(self.__name, "a") 26 | -------------------------------------------------------------------------------- /lib/module.py: -------------------------------------------------------------------------------- 1 | __author__ = 'VK OPS CREW ' 2 | 3 | 4 | class Module: 5 | def __init__(self, server1): 6 | self._server = server1 7 | self._log = lambda *args, **kwargs: self._server.log(*args, **kwargs) 8 | 9 | def _continue(self, routine, arguments): 10 | def continuation(): 11 | nonlocal routine, arguments 12 | return routine(*arguments) 13 | return continuation 14 | -------------------------------------------------------------------------------- /lib/policy.py: -------------------------------------------------------------------------------- 1 | from config import config 2 | 3 | __author__ = 'VK OPS CREW ' 4 | 5 | 6 | class Policy: 7 | 8 | def __init__(self, user=None, group=None, parameters=None, script=None): 9 | self.user = user 10 | self.group = group 11 | self.parameters = parameters 12 | self.script = script 13 | 14 | 15 | class PolicyManager: 16 | 17 | def __init__(self, key_manager, logger): 18 | self.policies = [] 19 | self.users = {} 20 | self.groups = {} 21 | self.cmds = {} 22 | self.key_manager = key_manager 23 | self.log = logger 24 | 25 | def add_policy(self, policy): 26 | ok_user = False 27 | ok_group = False 28 | if policy.user is not None: 29 | if self.key_manager.get_user_key(policy.user) is None: 30 | self.log("Can not find key for user %s" % str(policy.user), "E") 31 | return False 32 | else: 33 | ok_user = True 34 | if policy.group is not None: 35 | if not self.key_manager.has_group(policy.group): 36 | self.log("Can not find key for group %s" % str(policy.group), "E") 37 | return False 38 | else: 39 | ok_group = True 40 | if ok_user and ok_group: 41 | self.log("Ambiguous rule. Use either user or group.", "E") 42 | return False 43 | if policy.script is None: 44 | self.log("You should specify script to launch", "E") 45 | return False 46 | if policy.script.decode() not in config.scripts: 47 | self.log("Can not find script %s" % policy.script, "E") 48 | return False 49 | if policy.user in self.users: 50 | self.users[policy.user].append(policy) 51 | else: 52 | self.users[policy.user] = [policy] 53 | if policy.group in self.groups: 54 | self.groups[policy.group].append(policy) 55 | else: 56 | self.groups[policy.group] = [policy] 57 | if policy.script in self.cmds: 58 | self.cmds[policy.script].append(policy) 59 | else: 60 | self.cmds[policy.script] = [policy] 61 | self.policies.append(policy) 62 | return True 63 | 64 | def dump_policies(self): 65 | for policy in self.policies: 66 | result_string = "" 67 | if policy.user is not None: 68 | result_string += "-u %s " % policy.user.decode() 69 | if policy.group is not None: 70 | result_string += "-g %s " % policy.group.decode() 71 | for param in policy.parameters: 72 | result_string += "-p %s " % param.decode() 73 | result_string += policy.script.decode() + "\n" 74 | print(result_string) 75 | 76 | def check_request(self, request): 77 | user_to_check = request.signed_with 78 | cmd_to_check = request.script 79 | self.log("Checking permissions for user %s to execute %s" % (user_to_check, cmd_to_check), "N", 2) 80 | if cmd_to_check not in self.cmds: 81 | self.log("Can't check permissions for non-existing scripts %s" % cmd_to_check, "E") 82 | return False 83 | for policy in self.cmds[cmd_to_check]: 84 | if user_to_check == policy.user: 85 | return policy 86 | elif policy.group is not None: 87 | groups = self.key_manager.get_user_groups(user_to_check) 88 | if groups is not None: 89 | if policy.group in groups: 90 | return policy 91 | self.log("No actual policy rule found", "E", 2) 92 | return False 93 | -------------------------------------------------------------------------------- /lib/process.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import fcntl 3 | import os 4 | import select 5 | import signal 6 | 7 | from lib.module import Module 8 | from lib.scripts import launch_script 9 | 10 | __author__ = 'VK OPS CREW ' 11 | 12 | 13 | class Pool(Module): 14 | def __init__(self, server): 15 | super(Pool, self).__init__(server) 16 | assert self._server.pool is None 17 | self._server.pool = self 18 | self.__pids = {} 19 | signal.signal(signal.SIGCHLD, self.__sigchld) 20 | 21 | def __sigchld(self, signo, frame): 22 | assert signo == signal.SIGCHLD 23 | # self._log('SIGCHLD') 24 | self._server.set_sigchld (self._continue(self.__check_all, ())) 25 | # self._server.action_postpone(self._continue(self.__check_all, ())) 26 | 27 | def __check_all(self): 28 | for x in self.__pids.values(): 29 | yield self._continue(x.check, ()) 30 | return 31 | while True: 32 | try: 33 | pid, pid_exit, pid_usage = os.wait3(os.WNOHANG) 34 | except OSError as why: 35 | if why.errno == errno.ECHILD: 36 | break 37 | else: 38 | raise why 39 | # except ChildProcessError: 40 | # break 41 | if not pid: 42 | break 43 | self._log('process %d exited: %d' % (pid, pid_exit)) 44 | self._log('postpone check process: %s' % pid, 2) 45 | yield self._continue(self.__pids[pid].check, ()) 46 | 47 | def register(self, pid, process): 48 | assert pid not in self.__pids 49 | self.__pids[pid] = process 50 | 51 | def unregister(self, pid): 52 | assert pid in self.__pids 53 | del self.__pids[pid] 54 | 55 | 56 | class Process(Module): 57 | def __init__(self, server, popen, request): 58 | super(Process, self).__init__(server) 59 | self.__request = request 60 | self.__process = popen 61 | self.__data = b'' 62 | self.__pid = popen.pid 63 | self.__stdout = popen.stdout 64 | self.__stderr = popen.stderr 65 | self.__finished = False 66 | self._server.pool.register(self.__pid, self) 67 | 68 | fl = fcntl.fcntl(self.__stdout, fcntl.F_GETFL) 69 | fcntl.fcntl(self.__stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK) 70 | fl = fcntl.fcntl(self.__stderr, fcntl.F_GETFL) 71 | fcntl.fcntl(self.__stderr, fcntl.F_SETFL, fl | os.O_NONBLOCK) 72 | self._server.epoll.register(self.__stdout, lambda events: self.__handle(b"stdout", self.__stdout, events)) 73 | self._server.epoll.register(self.__stderr, lambda events: self.__handle(b"stderr", self.__stderr, events)) 74 | 75 | def __handle(self, type_of, handle, events): 76 | assert events 77 | if events & select.EPOLLIN: 78 | events &= ~ select.EPOLLIN 79 | self.__communicate(handle, type_of) 80 | if events & select.EPOLLHUP: 81 | events &= ~ select.EPOLLHUP 82 | yield self._continue(self.__request.queue.run_next, ()) 83 | if events: 84 | self._log("unhandled poll events: 0x%04x\n" % events, "D", 3) 85 | assert not events 86 | 87 | def __communicate(self, handle=None, type_of=None): 88 | if self.__finished: 89 | return 90 | if handle is None: 91 | for x, y in [(self.__stdout, b"stdout"), (self.__stderr, b"stderr")]: 92 | self.__communicate(x, y) 93 | return 94 | while True: 95 | from io import BlockingIOError 96 | try: 97 | data = handle.read() 98 | # print(type(data)) 99 | if type(data) != bytes: 100 | data = bytes(data, 'ascii') 101 | # print(type(data)) 102 | if data is None or not data: 103 | break 104 | except BlockingIOError: 105 | break 106 | except EOFError: 107 | break 108 | self._log("received from process: %s" % (''.join('%c' % x for x in data)), verbosity=3) 109 | data = data.split(b'\n') 110 | for chunk in data[:-1]: 111 | self.__request.client.write_log(type_of + b": " + self.__data + chunk) 112 | self.__request.data = b'' 113 | self.__data = data[-1] 114 | 115 | def check(self): 116 | self._log('check process: %s' % self.__pid, verbosity=2) 117 | finished = self.__process.poll() 118 | if finished is None: 119 | return 120 | self._log('subprocess poll: %s' % str(finished), "D", 3) 121 | self._server.wake() 122 | self.__communicate() 123 | if self.__data: 124 | self.__request.client.write_log(self.__data + b'%[noeoln]') 125 | self.__request.client.write_finish() 126 | self._server.epoll.unregister(self.__stdout) 127 | self._server.epoll.unregister(self.__stderr) 128 | self.__stdout.close() 129 | self.__stderr.close() 130 | self.__finished = True 131 | self._server.pool.unregister(self.__pid) 132 | # no yield from in python3.2 133 | for x in self.__request.queue.terminated(): 134 | yield x 135 | 136 | 137 | -------------------------------------------------------------------------------- /lib/request.py: -------------------------------------------------------------------------------- 1 | __author__ = 'VK OPS CREW ' 2 | 3 | 4 | class Request: 5 | def __init__(self, key, script, arguments, client): 6 | self.signed_with = key 7 | self.script = script 8 | self.arguments = arguments 9 | self.client = client 10 | self.queue = None 11 | self.index = None 12 | self.access = None 13 | 14 | -------------------------------------------------------------------------------- /lib/request_queue.py: -------------------------------------------------------------------------------- 1 | from lib.module import Module 2 | from lib.scripts import launch_script 3 | from lib.process import Process 4 | 5 | __author__ = 'VK OPS CREW ' 6 | 7 | 8 | class RequestQueue(Module): 9 | def __init__(self, server1, limit=1): 10 | super(RequestQueue, self).__init__(server1) 11 | self.__queue = [] 12 | self.__active = 0 13 | self.__prestart = 0 14 | self.__done = 0 15 | self.__limit = limit 16 | self.__index = 0 17 | self._server.action_add(self._continue(self.run_next, ())) 18 | 19 | @property 20 | def done(self): 21 | return self.__done 22 | 23 | def append(self, request): 24 | self.__index += 1 25 | request.index = self.__index 26 | request.queue = self 27 | self.__queue.append(request) 28 | request.client.write_queue(request.index - self.__done) 29 | self._log("new request #%d, queue size: %d" % (request.index, len(self.__queue))) 30 | 31 | def run_next(self): 32 | if not self.__active + self.__prestart < self.__limit: 33 | return 34 | if len(self.__queue) == 0: 35 | return 36 | self.__prestart += 1 37 | self._server.wake() 38 | request = self.__queue[0] 39 | self.__queue = self.__queue[1:] # TODO: optimize 40 | yield self._continue(self.__run, (request,)) 41 | 42 | def __run(self, request): 43 | assert self.__active < self.__limit 44 | self.__prestart -= 1 45 | if request.access is False: 46 | request.client.write_error(b'access_denied') 47 | return 48 | self.__active += 1 49 | request.client.write_start() 50 | request.process = Process(self._server, launch_script(request), request) 51 | 52 | def terminated(self): 53 | self._log('Active cleared, in queue: %d' % len(self.__queue), "D", 3) 54 | self.__active -= 1 55 | self.__done += 1 56 | yield self._continue(self.run_next, ()) 57 | 58 | -------------------------------------------------------------------------------- /lib/scripts.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from config import config 3 | import os 4 | 5 | __author__ = 'VK OPS CREW ' 6 | scripts = config.scripts 7 | 8 | 9 | def launch_script(request): 10 | policy = request.access 11 | 12 | if b"ALLOW_ARGUMENTS" in policy.parameters and scripts[request.script.decode()]["need_arguments"]: 13 | arguments = request.arguments 14 | else: 15 | arguments = [] 16 | 17 | if b"PROVIDE_USERNAME" in policy.parameters: 18 | os.environ["DELEGATE_USERNAME"] = request.signed_with.decode() 19 | 20 | process = subprocess.Popen( 21 | args=[scripts[request.script.decode()]["cmd_line"]] + scripts[request.script.decode()]["default_arguments"] + 22 | arguments, 23 | executable=scripts[request.script.decode()]["cmd_line"], 24 | stdin=open("/dev/null", "r"), 25 | stdout=subprocess.PIPE, 26 | stderr=subprocess.PIPE, 27 | cwd='/' 28 | ) 29 | 30 | return process 31 | -------------------------------------------------------------------------------- /lib/server.py: -------------------------------------------------------------------------------- 1 | import signal 2 | import queue 3 | from config import config as cfg 4 | 5 | __author__ = 'VK OPS CREW ' 6 | 7 | 8 | class Server: 9 | def __init__(self, logger1, keys1, policy1, config, queuer): 10 | self.log = logger1 11 | self.keys = keys1 12 | self.policy = policy1 13 | self.pool = None 14 | self.__config = config 15 | self.__queue = {} 16 | self.__finish = False 17 | self.__sleep = False 18 | self.__actions_start = [] 19 | self.__actions_sleep = [] 20 | self.__actions_now = queue.Queue() 21 | self.__queuer = queuer 22 | self.__sigchld = None 23 | # self.__actions_cron = [] 24 | 25 | signal.signal(signal.SIGTERM, lambda signo, frame: self.__signal(signo)) 26 | signal.signal(signal.SIGUSR1, lambda signo, frame: self.__signal(signo)) 27 | signal.signal(signal.SIGCHLD, lambda signo, frame: self.__signal(signo)) 28 | signal.signal(signal.SIGUSR2, lambda signo, frame: self.__signal(signo)) 29 | # TODO: more signals (HUP) 30 | 31 | def __signal(self, signo): 32 | if signo == signal.SIGTERM: 33 | # self.log("[SIGTERM]") 34 | self.__finish = True 35 | elif signo == signal.SIGUSR1: 36 | self.log.reopen() 37 | # self.log("logs rotated") 38 | elif signo == signal.SIGCHLD: 39 | self.log.reopen() 40 | self.wake() 41 | # self.log("caught SIGCHLD, ignore it") 42 | elif signo == signal.SIGUSR2: 43 | self.reload_config() 44 | else: 45 | self.log("unknown signal: %d" % signo, "E") 46 | self.__finish = True 47 | 48 | def action_add(self, action): 49 | self.__actions_start.append(action) 50 | 51 | def action_sleep_add(self, action): 52 | self.__actions_sleep.append(action) 53 | 54 | def action_postpone(self, action): 55 | self.__actions_now.put(action) 56 | 57 | def set_sigchld(self, action): 58 | self.__sigchld = action 59 | 60 | def wake(self): 61 | self.__sleep = False 62 | 63 | def reload_config(self): 64 | self.log("Rereading config", "L") 65 | from lib.config_loader import ConfigLoader 66 | from lib.keymanager import KeyManager 67 | from lib.policy import PolicyManager 68 | keys = KeyManager(self.log) 69 | 70 | policy = PolicyManager(keys, self.log) 71 | loader = ConfigLoader( 72 | self.log, 73 | self.__config["path_to_users"], 74 | self.__config["path_to_policies"], 75 | policy, 76 | keys 77 | ) 78 | if loader.read(): 79 | self.keys = keys 80 | self.policy = policy 81 | self.log("Successfully updated config", "L") 82 | else: 83 | self.log("Failed to reread config", "E") 84 | 85 | def run(self): 86 | actions = queue.Queue() 87 | try: 88 | while True: 89 | self.log("actions queue: %d" % actions.qsize(), "L", 4) 90 | if actions.empty(): 91 | if self.__finish: 92 | break 93 | if self.__sigchld is not None: 94 | actions.put(self.__sigchld) 95 | self.__sigchld = None 96 | if not self.__actions_now.empty(): 97 | actions = self.__actions_now 98 | self.__actions_now = queue.Queue() 99 | continue 100 | if self.__sleep: 101 | for x in self.__actions_sleep: 102 | actions.put(x) 103 | self.__sleep = True 104 | for x in self.__actions_start: 105 | actions.put(x) 106 | continue 107 | action = actions.get() 108 | result = action() 109 | if result is None: 110 | continue 111 | for x in result: 112 | actions.put(x) 113 | except KeyboardInterrupt: 114 | self.log("[Ctrl+C]") 115 | self.log("TODO: graceful exit (close all sockets etc)") 116 | 117 | def enqueue(self, request): 118 | request.access = self.policy.check_request(request) 119 | action = request.script.decode() 120 | token = cfg.scripts.get(action, {}).get('lock', '') 121 | if token is None: 122 | token = 'default' 123 | if token not in self.__queue: 124 | limit = 1 125 | if 'limit' in self.__config and token in self.__config['limit']: 126 | limit = self.__config['limit'][token] 127 | self.__queue[token] = self.__queuer(self, limit=limit) 128 | self.__queue[token].append(request) 129 | 130 | def __enter__(self): 131 | return self 132 | 133 | def __exit__(self, type, value, tb): 134 | signal.signal(signal.SIGTERM, signal.SIG_DFL) 135 | signal.signal(signal.SIGUSR1, signal.SIG_DFL) 136 | self.log("TODO: close all sockets") 137 | -------------------------------------------------------------------------------- /lib/sockets.py: -------------------------------------------------------------------------------- 1 | import select 2 | import socket 3 | import errno 4 | 5 | from config import config 6 | from lib.module import Module 7 | 8 | __author__ = 'VK OPS CREW ' 9 | cfg = config.config 10 | 11 | 12 | class ClientSocket(Module): 13 | 14 | def __init__(self, server1, connection, remote_addr, connector): 15 | super(ClientSocket, self).__init__(server1) 16 | self.__connected = True 17 | self.__socket = connection 18 | self.__remote_addr = remote_addr 19 | self.__buffer = b'' 20 | self.__write_buffer = [] 21 | self.__connector = connector(server1, self) 22 | self.__socket.setblocking(False) 23 | self._server.epoll.register(self.__socket, lambda events: self.__handle(events)) 24 | 25 | def handle_all(self): 26 | yield self._continue(self.__handle, (0, True)) 27 | 28 | def __handle(self, events, check_all=False): 29 | assert events or check_all 30 | if events & select.EPOLLIN or check_all: 31 | events &= ~ select.EPOLLIN 32 | while True: 33 | try: 34 | data = self.__socket.recv(4096) 35 | except socket.error as why: 36 | if why.args[0] in (socket.EAGAIN, socket.EWOULDBLOCK): 37 | break 38 | elif why.args[0] == errno.ECONNRESET: 39 | self.disconnect() 40 | return 41 | else: 42 | raise why 43 | if data is None or not data: 44 | self.disconnect() 45 | break 46 | assert data 47 | self._log("received from socket#%d: %s" % (self.__socket.fileno(), ' '.join('%02x' % x for x in data)), 48 | verbosity=3) 49 | 50 | data = data.split(b'\n') 51 | for chunk in data[:-1]: 52 | yield self._continue(self.execute, (self.__buffer + chunk,)) 53 | self.__buffer = b'' 54 | self.__buffer = data[-1] 55 | 56 | if events & select.EPOLLOUT or check_all: 57 | while len(self.__write_buffer): 58 | try: 59 | r = self.__socket.send(self.__write_buffer[0]) 60 | except socket.error as why: 61 | if why.args[0] in (socket.EAGAIN, socket.EWOULDBLOCK): 62 | break 63 | else: 64 | raise why 65 | if r < len(self.__write_buffer[0]): 66 | self.__write_buffer[0] = self.__write_buffer[0][r:] 67 | break 68 | self.__write_buffer = self.__write_buffer[1:] 69 | events &= ~ select.EPOLLOUT 70 | 71 | if events & select.EPOLLERR: 72 | self.disconnect() 73 | events &= ~ select.EPOLLERR 74 | 75 | if events & select.EPOLLHUP: 76 | self.disconnect() 77 | events &= ~ select.EPOLLHUP 78 | 79 | if events: 80 | self._log("unhandled poll events: 0x%04x\n" % events) 81 | assert not events 82 | 83 | def disconnect(self): 84 | if not self.__connected: 85 | return 86 | self._log("connection #%d closed" % self.__socket.fileno()) 87 | self._server.epoll.unregister(self.__socket) 88 | self.__socket.close() 89 | self.__write_buffer = [] 90 | self.__connected = False 91 | 92 | def write(self, data): 93 | if len(data) == 0 or not self.__connected: 94 | return 95 | if self.__write_buffer: 96 | self.__write_buffer.append(data) 97 | return 98 | try: 99 | self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 100 | r = self.__socket.send(data) 101 | self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0) 102 | except socket.error as why: 103 | if why.args[0] in (socket.EAGAIN, socket.EWOULDBLOCK): 104 | r = 0 105 | elif why.args[0] == errno.ECONNRESET: 106 | self.disconnect() 107 | return 108 | else: 109 | raise why 110 | if r == len(data): 111 | return 112 | self.__write_buffer.append(data[r:]) 113 | 114 | def execute(self, command): 115 | self._log("command from connection #%d: %s" % (self.__socket.fileno(), command.decode('iso8859-1')), verbosity=2) 116 | self.__connector(command) 117 | 118 | 119 | class ServerSocket(Module): 120 | def __init__(self, server1, connector): 121 | super(ServerSocket, self).__init__(server1) 122 | self.__connector = connector 123 | self.__socket = socket.socket( 124 | type=socket.SOCK_STREAM | socket.SOCK_NONBLOCK 125 | ) 126 | self.__socket.setsockopt (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 127 | self.__socket.bind((cfg["serve"], cfg['port'])) 128 | self.__socket.listen(5) 129 | self._server.epoll.register(self.__socket, lambda events: self.__handle(events)) 130 | 131 | def __handle(self, events): 132 | assert events 133 | if events & select.EPOLLIN: 134 | events &= ~ select.EPOLLIN 135 | self._log("ServerSocket.__handle", "L", 3) 136 | while True: 137 | try: 138 | self._log("[debug] before try", "L", 3) 139 | client, remote_addr = self.__socket.accept() 140 | self._log("[debug] after try", "L", 3) 141 | except socket.error as why: 142 | if why.args[0] in (socket.EAGAIN, socket.EWOULDBLOCK): 143 | self._log("no more from server socket", "L", 3) 144 | break 145 | else: 146 | self._log("unknown socket error: " + str(why)) 147 | raise why 148 | # except Exception as e: 149 | # self._log(e, "E") 150 | # break 151 | self._log("accepted client [%s]: %s" % (remote_addr, client)) 152 | client = ClientSocket(self._server, client, remote_addr, self.__connector) 153 | self._log("[debug] ClientSocket created", "L", 3) 154 | yield self._continue(client.handle_all, ()) 155 | self._log("[debug] post-action added", "L", 3) 156 | assert not events 157 | --------------------------------------------------------------------------------