Browse Source

introduce node ACLs and playitsafe() check function

Helge Jung 5 years ago
parent
commit
cb8b14b5bd
2 changed files with 163 additions and 27 deletions
  1. 1 0
      .gitignore
  2. 162 27
      modules/ffpb.py

+ 1 - 0
.gitignore

@@ -6,5 +6,6 @@ build/
 logs/
 alfred.json
 highscoredata
+nodes.acl
 nodes.monitored
 nodes.seen

+ 162 - 27
modules/ffpb.py

@@ -5,6 +5,7 @@ import willie
 import datetime
 import difflib
 from email.utils import mktime_tz
+from fnmatch import fnmatch
 import git
 import netaddr
 import json
@@ -26,6 +27,8 @@ peers_repo = None
 monitored_nodes = None
 highscores = None
 
+nodeaccess = None
+
 alfred_method = None
 alfred_data = None
 alfred_update = datetime.datetime(1970,1,1,23,42)
@@ -74,7 +77,7 @@ class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
 	pass
 
 def setup(bot):
-	global msgserver, peers_repo, alfred_method, highscores, monitored_nodes
+	global msgserver, peers_repo, alfred_method, highscores, monitored_nodes, nodeaccess
 
 	# signal begin of setup routine
 	bot.memory['ffpb_in_setup'] = True
@@ -95,6 +98,9 @@ def setup(bot):
 	seen_nodes = shelve.open('nodes.seen', writeback=True)
 	bot.memory['seen_nodes'] = seen_nodes
 
+	# load list of node ACL from disk (used in playitsafe())
+	nodeaccess = shelve.open('nodes.acl', writeback=True)
+
 	# no need to configure anything else if the ffpb config section is missing
 	if not bot.config.has_section('ffpb'):
 		bot.memory['ffpb_in_setup'] = False
@@ -129,7 +135,7 @@ def setup(bot):
 	bot.memory['ffpb_in_setup'] = False
 
 def shutdown(bot):
-	global msgserver, highscores, monitored_nodes
+	global msgserver, highscores, monitored_nodes, nodeaccess
 
 	# store highscores
 	if not highscores is None:
@@ -143,6 +149,12 @@ def shutdown(bot):
 		monitored_nodes.close()
 		monitored_nodes = None
 
+	# store node acl
+	if not nodeaccess is None:
+		nodeaccess.sync()
+		nodeaccess.close()
+		nodeaccess = None
+
 	# store seen nodes
 	if 'seen_nodes' in bot.memory and bot.memory['seen_nodes'] != None:
 		bot.memory['seen_nodes'].close()
@@ -184,6 +196,145 @@ def ffpb_help(bot, trigger):
 
 	bot.say("Allgemeine Hilfe gibt's mit !help - ohne Parameter.")
 
+def playitsafe(bot, trigger,
+	botadmin=False, admin_channel=False, via_channel=False, via_privmsg=False, need_op=False, node=None,
+	reply_directly=True, debug_user=None, debug_ignorebotadmin=False):
+	"""helper: checks that the triggering user has the necessary rights
+
+	Returns true if everything is okay. If it's not, a reply is send via the bot and false is returned.
+	"""
+
+	if via_channel and via_privmsg:
+		raise Exception('Der Entwickler ist ein dummer, dummer Junge (playitsafe hat via_channel und via_privmsg gleichzeitig gesetzt).')
+
+	user = trigger.nick if debug_user is None else debug_user
+	user = user.lower()
+
+	# botadmin: you need to be configured as a bot admin
+	if botadmin and not trigger.admin:
+		if reply_directly: bot.say('Du brauchst Super-Kuh-Kräfte um dieses Kommando auszuführen.')
+		return False
+
+	# via_channel: the request must not be a private conversation
+	if via_channel and trigger.is_privmsg:
+		if reply_directly: bot.say('Bitte per Channel - mehr Transparenz wagen und so!')
+		return False
+
+	# via_privmsg: the request must be a private conversation
+	if via_privmsg and not trigger.is_privmsg:
+		if reply_directly: bot.say('Solche Informationen gibt es nur per PM, da bin ich ja schon ein klein wenig sensibel ...')
+		return False
+
+	# need_op: if the message is in a channel, check that the user has OP there
+	if need_op and (not trigger.is_privmsg) and (not user in bot.ops[trigger.sender]):
+		if reply_directly: bot.say('Keine Zimtschnecke, keine Kekse.')
+		return False
+
+	# node: check that the user is whitelisted (or is admin)
+	if not node is None and (debug_ignorebotadmin or not trigger.admin):
+		acluser = [ x for x in nodeaccess if x.lower() == user ]
+		acluser = acluser[0] if len(acluser) == 1 else None
+		if nodeaccess is None or acluser is None:
+			if reply_directly: bot.reply('You! Shall! Not! Access!')
+			return False
+
+		nodeid = node['node_id'] if 'node_id' in node else None
+		matched = False
+		for x in nodeaccess[acluser]:
+			if x == nodeid or fnmatch(node['hostname'], x):
+				matched = True
+				break
+
+		if not matched:
+			if reply_directly: bot.reply('Mach das doch bitte auf deinen Knoten, kthxbye.')
+			return False
+
+	return True
+
+@willie.module.commands('nodeacl')
+def ffpb_nodeacl(bot, trigger):
+	"""Configure ACL for nodes."""
+
+	if not playitsafe(bot, trigger, botadmin=True):
+		# the check function already gives a bot reply, just exit here
+		return
+
+	# ensure the user gave arguments (group 2 is the concatenation of all following groups)
+	if trigger.group(2) is None or len(trigger.group(2)) == 0:
+		bot.say('Sag doch was du willst ... einmal mit Profis arbeiten, ey -.-')
+		return
+
+	# read additional arguments
+	cmd = trigger.group(3).lower()
+
+	if cmd == 'list':
+		user = trigger.group(4)
+		if user is None:
+			bot.say('ACLs gesetzt für die User: ' + ', '.join([x for x in nodeaccess]))
+			return
+
+		user = user.lower()
+		uid = [ x for x in nodeaccess if x.lower() == user ]
+		if len(uid) == 0:
+			bot.say('Für \'{0}\' ist keine Node ACL gesetzt.'.format(user))
+			return
+
+		bot.say('Node ACL für \'{0}\' = \'{1}\''.format(uid[0], '\', \''.join(nodeaccess[uid[0]])))
+		return
+
+	if cmd in [ 'add', 'del', 'check' ]:
+		user = trigger.group(4)
+		value = trigger.group(5)
+
+		if user is None or value is None:
+			bot.say('Du bist eine Pappnase - User und Knoten, bitte.')
+			return
+
+		user = str(user)
+		print('NodeACL ' + cmd + ' \'' + value + '\' for user \'' + user + '\'')
+		uid = [ x for x in nodeaccess if x == user or x.lower() == user ]
+		if cmd == 'add':
+			uid = uid[0] if len(uid) > 0 else user
+			if not uid in nodeaccess:
+				nodeaccess[uid] = []
+			if not value in nodeaccess[uid]:
+				nodeaccess[uid].append(value)
+				bot.say('201 nodeACL \'{0}\' +\'{1}\''.format(uid, value))
+			else:
+				bot.say('304 nodeACL \'{0}\' contains \'{1}\''.format(uid, value))
+
+		elif cmd == 'del':
+			if len(uid) == 0:
+				bot.say('404 nodeACL \'{0}\''.format(uid))
+				return
+			if value in nodeaccess[uid]:
+				nodeaccess[uid].remove(value)
+				bot.say('200 nodeACL \'{0}\' -\'{1}\''.format(uid, value))
+			else:
+				bot.say('404 nodeACL \'{0}\' does not contain \'{1}\''.format(uid, value))
+
+		elif cmd == 'check':
+			if len(uid) == 0:
+				bot.say('Nope, keine ACL gesetzt.')
+				return
+
+			node = ffpb_findnode(value)
+			if node is None:
+				bot.say('Nope, kein Plan was für ein Knoten das ist.')
+				return
+
+			result = playitsafe(bot, trigger, debug_user=uid[0], debug_ignorebotadmin=True, node=node, reply_directly=False)
+			if result == True:
+				bot.say('Jupp.')
+			elif result == False:
+				bot.say('Nope.')
+			else:
+				bot.say('Huh? result=' + str(result))
+
+		return
+
+	bot.say('Unbekanntes Kommando. Probier "list [user]", "add user value" oder "del user value". Value kann node_id oder hostname-Maske sein.')
+
 def ffpb_findnode(name):
 	"""helper: try to identify the node the user meant by the given name"""
 
@@ -397,21 +548,16 @@ def ffpb_debug_alfred(bot, trigger):
 def ffpb_peerdata(bot, trigger):
 	"""Show ALFRED data of the given node."""
 
-	# user must be a bot admin
-	if (not trigger.admin):
-		bot.say('I wont leak (possibly) sensitive data to you.')
-		return
-
-	# query must be a PM or as OP in the channel
-	if (not trigger.is_privmsg) and (not trigger.nick in bot.ops[trigger.sender]):
-		bot.say('Kein Keks? Keine Daten.')
-		return
-
 	# identify node or bail out
 	target_name = trigger.group(2)
 	node = ffpb_findnode_from_botparam(bot, target_name)
 	if node is None: return
 
+	# query must be a PM or as OP in the channel
+	if not playitsafe(bot, trigger, need_op=True, node=node):
+		# the check function already gives a bot reply, just exit here
+		return
+
 	# reply each key in the node's data
 	for key in node:
 		if key in [ 'hostname' ]: continue
@@ -938,25 +1084,14 @@ def ffpb_remoteexec(bot, trigger):
 	target_name = bot_params[0]
 	target_cmd = bot_params[1]
 
-	# remote execution may only be triggered by bot admins
-	if not trigger.admin:
-		bot.say('I can haz sudo?')
-		return
-
-	# make sure remote execution is done in public
-	if trigger.is_privmsg:
-		bot.say('Bitte per Channel.')
-		return
-
-	# double-safety: user must be op in the channel, too (hoping for NickServ authentication)
-	if not trigger.nick in bot.ops[trigger.sender]:
-		bot.say('Geh weg.')
-		return
-
 	# identify requested node or bail out
 	node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
 	if node is None: return
 
+	# check ACL
+	if not playitsafe(bot, trigger, via_channel=True, node=node):
+		return
+
 	# use the node's first non-linklocal address
 	target = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
 	target_alias = node["hostname"]