Commits

Michael K committed 6d8c28c Merge

Merging up TIL clone

  • Participants
  • Parent commits 15cc5e6, 42a8983

Comments (0)

Files changed (27)

-.*\.json
+config.json
 .*\.log

File buildgroups.py

-#!/usr/bin/python
-import ts3tools, announce
-from ldaptools import LDAPTools
-from keytools import KeyTools
-import json
-import logging
-import time
-from logging import handlers
-from ldap import MOD_ADD, MOD_DELETE, MOD_REPLACE
-
-# Load configuration
-with open("config.json") as fh:
-	config=json.loads(fh.read())
-assert(config)
-
-# Set up all classes
-ldaptools = LDAPTools(config)
-
-
-if __name__ == "__main__":
-	logger = logging.getLogger("buildgroups")
-	logger.setLevel(logging.DEBUG)
-	fh = logging.FileHandler("./logs/buildgroups_%d.log" % time.time())
-	formatter = logging.Formatter('%(asctime)s - %(message)s')
-	fh.setFormatter(formatter)
-	logger.addHandler(fh)
-
-	allgroups = config["groups"]["opengroups"] + config["groups"]["closedgroups"]
-	for group in allgroups:
-			members = ldaptools.getusers("authGroup=%s" % group)
-			attrs = {}
-			attrs["cn"] = str(group)
-			attrs["description"] = str(("Autogenerated %s group") % group)
-			attrs["member"] = map(lambda x: str(("uid=%s,%s") % (x.uid[0], config["ldap"]["memberdn"])), members)
-			ldaptools.addgroup(attrs)

File config.example.json

+{
+	"auth": {
+		"alliance": "Confederation of xXPIZZAXx",
+		"allianceshort": "PIZZA",
+		"domain": "xxpizzaxx.com"
+	},
+	"pingbot": {
+		"username": "",
+		"passwd": "",
+		"domain": "xxpizzaxx.com"
+	},
+
+	"keytools": {
+		"executorkeyid": "",
+		"executorkeyvcode": "",
+		"alliancelimit": 4.9
+	},
+
+	"groups": {
+		"closedgroups": [
+			"admin",
+			"ping",
+			"capital",
+			"timerboard"
+		],
+		"opengroups": [
+			"social",
+			"dota"
+		],
+		"publicgroups": [
+			"dota"
+		]
+	},
+	"ts3": {
+		"user": "serveradmin",
+		"password": "",
+		"server": "localhost",
+		"port": 10011,
+		"servergroups":	{
+			"full": "7",
+			"ally": "14",
+			"none": "8"
+		}
+	},
+
+	"ldap": {
+		"server": "ldap://localhost/",
+		"admin": "cn=admin,dc=yoursite,dc=com",
+		"password": "",
+		"basedn": "dc=yoursite,dc=com",
+		"memberdn": "ou=People,dc=yoursite,dc=com",
+		"groupdn": "ou=Groups,dc=yoursite,dc=com"
+	},
+
+	"skillindexer": {
+		"server": "localhost",
+		"user": "",
+		"password": "",
+		"database": ""
+	},
+	"mumble": {
+		"server": "mumble.yoursite.com"
+	},
+	"reddit": {
+		"comment": "REMOVE ENTIRE SECTION IF YOU'RE NOT USING REDDIT VERIFICATION",
+		"clientname":	"",
+		"clientid": 	"",
+		"clientsecret":	"",
+		"redirect_base": "http://host:port",
+		"statekey":	""
+	},
+	"apikeys": [
+		"REMOVEME"
+		]
+}

File config/LDAPauth.ini

 display_attr = characterName
 group_cn =
 group_attr =
+group_list_attr = authGroup
 ldap_filter = |(accountStatus=Internal)(accountStatus=Ally)
 
 ;Murmur configuration

File config/LDAPauth.py

 import urllib2
 import logging
 import ConfigParser
+import eveapi
+
+
+CORPORATIONS_TICKER = {}
 
 from threading  import Timer
 from optparse   import OptionParser
                     ('number_attr', str, 'RoomNumber'),
                     ('display_attr', str, 'displayName'),
                     ('group_cn', str, 'ou=Groups,dc=example,dc=org'),
-                    ('group_attr', str, 'member')),
+                    ('group_attr', str, 'member'),
+		    ('group_list_attr', str, '')),
 
             'user':(('id_offset', int, 1000000000),
                     ('reject_on_error', x2bool, True)),
-           
+
             'ice':(('host', str, '127.0.0.1'),
                    ('port', int, 6502),
                    ('slice', str, 'Murmur.ice'),
                    ('secret', str, ''),
                    ('watchdog', int, 30)),
-                   
+
             'iceraw':None,
-                   
+
             'murmur':(('servers', lambda x:map(int, x.split(',')), []),),
             'glacier':(('enabled', x2bool, False),
                        ('user', str, 'ldapauth'),
                        ('password', str, 'secret'),
                        ('host', str, 'localhost'),
                        ('port', int, '4063')),
-                       
+
             'log':(('level', int, logging.DEBUG),
                    ('file', str, 'LDAPauth.log'))}
- 
+
 #
 #--- Helper classes
 #
         cfg = ConfigParser.ConfigParser()
         cfg.optionxform = str
         cfg.read(filename)
-        
+
         for h,v in default.iteritems():
             if not v:
                 # Output this whole section as a list of raw key/value tuples
                         self.__dict__[h].__dict__[name] = conv(cfg.get(h, name))
                     except (ValueError, ConfigParser.NoSectionError, ConfigParser.NoOptionError):
                         self.__dict__[h].__dict__[name] = vdefault
-                    
+
 
 def do_main_program():
     #
         slicedir = ['-I' + slicedir]
     Ice.loadSlice('', slicedir + [cfg.ice.slice])
     import Murmur
-    
+
     class LDAPAuthenticatorApp(Ice.Application):
         def run(self, args):
             self.shutdownOnInterrupt()
-            
+
             if not self.initializeIceConnection():
                 return 1
 
             if cfg.ice.watchdog > 0:
                 self.failedWatch = True
                 self.checkConnection()
-                
+
             # Serve till we are stopped
             self.communicator().waitForShutdown()
             self.watchdog.cancel()
-            
+
             if self.interrupted():
                 warning('Caught interrupt, shutting down')
-                
+
             return 0
-        
+
         def initializeIceConnection(self):
             """
             Establishes the two-way Ice connection and adds the authenticator to the
             configured servers
             """
             ice = self.communicator()
-            
+
             if cfg.ice.secret:
                 debug('Using shared ice secret')
                 ice.getImplicitContext().put("secret", cfg.ice.secret)
             elif not cfg.glacier.enabled:
                 warning('Consider using an ice secret to improve security')
-                
+
             if cfg.glacier.enabled:
                 #info('Connecting to Glacier2 server (%s:%d)', glacier_host, glacier_port)
                 error('Glacier support not implemented yet')
                 #TODO: Implement this
-    
+
             info('Connecting to Ice server (%s:%d)', cfg.ice.host, cfg.ice.port)
             base = ice.stringToProxy('Meta:tcp -h %s -p %d' % (cfg.ice.host, cfg.ice.port))
             self.meta = Murmur.MetaPrx.uncheckedCast(base)
-        
+
             adapter = ice.createObjectAdapterWithEndpoints('Callback.Client', 'tcp -h %s' % cfg.ice.host)
             adapter.activate()
-            
+
             metacbprx = adapter.addWithUUID(metaCallback(self))
             self.metacb = Murmur.MetaCallbackPrx.uncheckedCast(metacbprx)
-            
+
             authprx = adapter.addWithUUID(LDAPAuthenticator())
             self.auth = Murmur.ServerUpdatingAuthenticatorPrx.uncheckedCast(authprx)
-            
+
             return self.attachCallbacks()
-        
+
         def attachCallbacks(self, quiet = False):
             """
             Attaches all callbacks for meta and authenticators
             """
-            
+
             # Ice.ConnectionRefusedException
             #debug('Attaching callbacks')
             try:
                 if not quiet: info('Attaching meta callback')
 
                 self.meta.addCallback(self.metacb)
-                
+
                 for server in self.meta.getBootedServers():
                     if not cfg.murmur.servers or server.id() in cfg.murmur.servers:
                         if not quiet: info('Setting authenticator for virtual server %d', server.id())
                         server.setAuthenticator(self.auth)
-                        
+
             except (Murmur.InvalidSecretException, Ice.UnknownUserException, Ice.ConnectionRefusedException), e:
                 if isinstance(e, Ice.ConnectionRefusedException):
                     error('Server refused connection')
                 else:
                     # We do not actually want to handle this one, re-raise it
                     raise e
-                
+
                 self.connected = False
                 return False
 
             self.connected = True
             return True
-        
+
         def checkConnection(self):
             """
             Tries reapplies all callbacks to make sure the authenticator
             # Renew the timer
             self.watchdog = Timer(cfg.ice.watchdog, self.checkConnection)
             self.watchdog.start()
-        
+
     def checkSecret(func):
         """
         Decorator that checks whether the server transmitted the right secret
         """
         if not cfg.ice.secret:
             return func
-        
+
         def newfunc(*args, **kws):
             if 'current' in kws:
                 current = kws["current"]
             else:
                 current = args[-1]
-            
+
             if not current or 'secret' not in current.ctx or current.ctx['secret'] != cfg.ice.secret:
                 error('Server transmitted invalid secret. Possible injection attempt.')
                 raise Murmur.InvalidSecretException()
-            
+
             return func(*args, **kws)
-        
+
         return newfunc
 
     def fortifyIceFu(retval = None, exceptions = (Ice.Exception,)):
         value. This helps preventing the authenticator getting stuck in
         critical code paths. Only exceptions that are instances of classes
         given in the exceptions list are not caught.
-        
+
         The default is to catch all non-Ice exceptions.
         """
         def newdec(func):
 
             return newfunc
         return newdec
-                
+
     class metaCallback(Murmur.MetaCallback):
         def __init__(self, app):
             Murmur.MetaCallback.__init__(self)
                     if hasattr(e, "unknown") and e.unknown != "Murmur::InvalidSecretException":
                         # Special handling for Murmur 1.2.2 servers with invalid slice files
                         raise e
-                    
+
                     error('Invalid ice secret')
                     return
             else:
                     return
                 except Ice.ConnectionRefusedException:
                     self.app.connected = False
-            
+
             debug('Server shutdown stopped a virtual server')
-    
+
     if cfg.user.reject_on_error: # Python 2.4 compat
         authenticateFortifyResult = (-1, None, None)
     else:
         authenticateFortifyResult = (-2, None, None)
-        
+
     class LDAPAuthenticator(Murmur.ServerUpdatingAuthenticator):
         def __init__(self):
             Murmur.ServerUpdatingAuthenticator.__init__(self)
             """
             This function is called to authenticate a user
             """
-            
+
             # Search for the user in the database
             FALL_THROUGH = -2
             AUTH_REFUSED = -1
-            
+
             if name == 'SuperUser':
                 debug('Forced fall through for SuperUser')
                 return (FALL_THROUGH, None, None)
-            
+
             #Otherwise, let's check the LDAP server
             uid = None
             try:
                 #Attempt to bind to LDAP server with user-provided credentials
                 ldap_conn = ldap.initialize(cfg.ldap.ldap_uri, 0)
                 ldap_conn.bind_s("%s=%s,%s" % (cfg.ldap.username_attr, name, cfg.ldap.users_dn), pw)
-                res = ldap_conn.search_s(cfg.ldap.users_dn, ldap.SCOPE_SUBTREE, '(%s=%s)' % (cfg.ldap.username_attr, name), [cfg.ldap.number_attr, cfg.ldap.display_attr])
+                res = ldap_conn.search_s(cfg.ldap.users_dn, ldap.SCOPE_SUBTREE, '(%s=%s)' % (cfg.ldap.username_attr, name), [cfg.ldap.number_attr, cfg.ldap.display_attr, 'corporation', cfg.ldap.group_list_attr ])
                 match = res[0] #Only interested in the first result, as there should only be one match
-                
+
                 #Parse the user information
                 uid = int(match[1][cfg.ldap.number_attr][0])
                 displayName = match[1][cfg.ldap.display_attr][0]
+                corporation = match[1]['corporation'][0]
                 debug('User match found, display "' + displayName + '" with UID ' + repr(uid))
-                
-                #Optionally check groups
+
+                #Optionally check required group
                 if cfg.ldap.group_cn != "" :
                     debug('Checking group membership for ' + name)
-                    
+
                     #Search for user in group
+		    debug('Searching with query: (%s=%s=%s,%s)' % (cfg.ldap.group_attr, cfg.ldap.username_attr, name, cfg.ldap.users_dn))
                     res = ldap_conn.search_s(cfg.ldap.group_cn, ldap.SCOPE_SUBTREE, '(%s=%s=%s,%s)' % (cfg.ldap.group_attr, cfg.ldap.username_attr, name, cfg.ldap.users_dn), [cfg.ldap.number_attr, cfg.ldap.display_attr])
-                    
+
                     # Check if the user is a member of the group
                     if len(res) < 1:
                         debug('User ' + name + ' failed with no group membership')
                         return (AUTH_REFUSED, None, None)
-                    
+
                 #Unbind and close connection
                 ldap_conn.unbind()
-                
+
             #What follows below are various what-if scenarios: authentication failures and successes
-                     
+
             #LDAP bind failed - expected to happen if bad login
-            except ldap.INVALID_CREDENTIALS: 
+            except ldap.INVALID_CREDENTIALS:
                     warning("User " + name + " failed with wrong password")
                     return (AUTH_REFUSED, None, None)
-    
+
             #If we get here, the login is correct.
+
+            if corporation not in CORPORATIONS_TICKER:
+                try:
+                    api = eveapi.EVEAPIConnection()
+                    character_id = api.eve.CharacterID(names=displayName).characters[0].characterID
+                    character_sheet = api.eve.CharacterInfo(characterID=character_id)
+                    corporation_sheet = api.corp.CorporationSheet(corporationID=character_sheet.corporationID)
+                    ticker = CORPORATIONS_TICKER[corporation] = corporation_sheet.ticker
+                except Exception, e:
+                    warning('Could not retrieve Ticker for corporation {corporation}'.format(corporation=corporation))
+                    ticker = 'NOTICKER'
+            else:
+                ticker = CORPORATIONS_TICKER[corporation]
+
+	    # Get user's groups
+            member_groups = match[1].get('authGroup', [])
+	    member_groups.append(corporation.lower())
+            debug(member_groups)
+
+
             #Add the user/id combo to cache, then accept:
             self.name_uid_cache[displayName] = uid
             debug("Login accepted for " + name)
-            return (uid + cfg.user.id_offset, displayName, [])
-            
+	    debug("Returning: %s, %s, %s " % (uid + cfg.user.id_offset, ticker + ' - ' + displayName, member_groups))
+            return (uid + cfg.user.id_offset, ticker + ' - ' + displayName, member_groups)
+
         @fortifyIceFu((False, None))
         @checkSecret
         def getInfo(self, id, current = None):
             """
             Gets called to fetch user specific information
             """
-            
+
             # We do not expose any additional information so always fall through
             debug('getInfo for %d -> denied', id)
             return (False, None)
 
-    
+
         @fortifyIceFu(-2)
         @checkSecret
         def nameToId(self, name, current = None):
             Gets called to get the id for a given username
             """
             FALL_THROUGH = -2
-            
+
             if name == 'SuperUser':
                 debug('nameToId SuperUser -> forced fall through')
                 return FALL_THROUGH
-            
+
             if name in self.name_uid_cache:
                 uid = self.name_uid_cache[name] + cfg.user.id_offset
                 debug("nameToId %s (cache) -> %d", name, uid)
                 return uid
-            
+
             ldap_conn = ldap.initialize(cfg.ldap.ldap_uri, 0) #Anon search
             res = ldap_conn.search_s(cfg.ldap.users_dn, ldap.SCOPE_SUBTREE, '(%s=%s)' % (cfg.ldap.display_attr, name), [cfg.ldap.number_attr])
-            
+
             #If user found, return the ID
             if len(res) == 1:
                 uid = int(res[0][1][cfg.ldap.number_attr][0]) + cfg.user.id_offset
             else:
                 debug('nameToId %s -> ?', name)
                 return FALL_THROUGH
-            
+
             return result
-        
-        
+
+
         @fortifyIceFu("")
         @checkSecret
         def idToName(self, id, current = None):
             """
             Gets called to get the username for a given id
             """
-            
+
             FALL_THROUGH = ""
 
             # Make sure the ID is in our range and transform it to the actual LDAP user id
             if id < cfg.user.id_offset:
                 debug('idToName %d -> fall through', id)
-                return FALL_THROUGH 
-            
+                return FALL_THROUGH
+
             ldapid = id - cfg.user.id_offset
-            
+
             for name, uid in self.name_uid_cache.iteritems():
                 if uid == ldapid:
                     if name == 'SuperUser':
                         debug('idToName %d -> "SuperUser" catched', id)
                         return FALL_THROUGH
-                    
+
                     debug('idToName %d -> "%s"', id, name)
                     return name
-                
+
             debug('idToName %d -> ?', id)
             return FALL_THROUGH
-         
-            
+
+
         @fortifyIceFu("")
         @checkSecret
         def idToTexture(self, id, current = None):
             """
             Gets called to get the corresponding texture for a user
             """
-            
+
             FALL_THROUGH = ""
             debug('idToTexture %d -> fall through', id)
             return FALL_THROUGH
-            
+
         @fortifyIceFu(-2)
         @checkSecret
         def registerUser(self, name, current = None):
             """
             Gets called when the server is asked to register a user.
             """
-            
+
             FALL_THROUGH = -2
             debug('registerUser "%s" -> fall through', name)
             return FALL_THROUGH
             """
             Gets called when the server is asked to unregister a user.
             """
-            
+
             FALL_THROUGH = -1
             # Return -1 to fall through to internal server database, we will not modify the LDAP directory
             # but we can make murmur delete all additional information it got this way.
             FALL_THROUGH = {}
             debug('getRegisteredUsers -> fall through')
             return FALL_THROUGH
-        
+
         @fortifyIceFu(-1)
         @checkSecret
         def setInfo(self, id, info, current = None):
             Gets called when the server is supposed to save additional information
             about a user to his database
             """
-            
+
             FALL_THROUGH = -1
             # Return -1 to fall through to the internal server handler. We do not store
             # any information in LDAP
             debug('setInfo %d -> fall through', id)
             return FALL_THROUGH
-        
+
         @fortifyIceFu(-1)
         @checkSecret
         def setTexture(self, id, texture, current = None):
             Gets called when the server is asked to update the user texture of a user
             """
             FALL_THROUGH = -1
-            
+
             # We do not store textures in LDAP
             debug('setTexture %d -> fall through', id)
             return FALL_THROUGH
-        
+
     class CustomLogger(Ice.Logger):
         """
         Logger implementation to pipe Ice log messages into
         out own log
         """
-        
+
         def __init__(self):
             Ice.Logger.__init__(self)
             self._log = getLogger('Ice')
-            
+
         def _print(self, message):
             self._log.info(message)
-            
+
         def trace(self, category, message):
             self._log.debug('Trace %s: %s', category, message)
-            
+
         def warning(self, message):
             self._log.warning(message)
-            
+
         def error(self, message):
             self._log.error(message)
 
     initdata.properties = Ice.createProperties([], initdata.properties)
     for prop, val in cfg.iceraw:
         initdata.properties.setProperty(prop, val)
-        
+
     initdata.properties.setProperty('Ice.ImplicitContext', 'Shared')
     initdata.logger = CustomLogger()
-    
+
     app = LDAPAuthenticatorApp()
     state = app.main(sys.argv[:1], initData = initdata)
     info('Shutdown complete')
     parser.add_option('-a', '--app', action='store_true', dest = 'force_app',
                       help = 'do not run as daemon', default = False)
     (option, args) = parser.parse_args()
-    
+
     if option.force_daemon and option.force_app:
         parser.print_help()
         sys.exit(1)
-        
+
     # Load configuration
     try:
         cfg = config(option.ini, default)
     except Exception, e:
         print>>sys.stderr, 'Fatal error, could not load config file from "%s"' % cfgfile
         sys.exit(1)
-    
-    
+
+
     # Initialize logger
     if cfg.log.file:
         try:
             sys.exit(1)
     else:
         logfile = logging.sys.stderr
-        
-            
+
+
     if option.verbose:
         level = cfg.log.level
     else:
         level = logging.ERROR
-    
+
     logging.basicConfig(level = level,
                         format='%(asctime)s %(levelname)s %(message)s',
                         stream = logfile)
-        
+
     # As the default try to run as daemon. Silently degrade to running as a normal application if this fails
-    # unless the user explicitly defined what he expected with the -a / -d parameter. 
+    # unless the user explicitly defined what he expected with the -a / -d parameter.
     try:
         if option.force_app:
             raise ImportError # Pretend that we couldn't import the daemon lib

File pizza_auth/adminshell.py

File contents unchanged.

File pizza_auth/announce.py

-import xmpp
+import xmpp, dns, dns.resolver, sys
+from time import gmtime,strftime
 from ldaptools import LDAPTools
 class pingbot():
 	def __init__(self, config):
 		jidparams['password'] = self.passwd
 
 		jid=xmpp.protocol.JID(jidparams['jid'])
-		cl=xmpp.Client(jid.getDomain(), debug=[])
+
+		try:
+			r = dns.resolver.query('_xmpp-client._tcp.%s' % server, dns.rdatatype.SRV)
+			if len(r)==1:
+				server = r[0].target.to_text().rstrip(".")
+		except:
+			pass
+
+		cl=xmpp.Client(server, debug=[])
 
 		con=cl.connect()
 		if not con:
 		print 'sent message with id',id
 
 	def generatemessage(self, sender, to, message):
+		utctime = strftime("%X +0000", gmtime())
 		result = message
 		result = result+"\n\n"
-		result = result+"== broadcast from %s to %s ==" % (sender, to)
+		result = result+"== broadcast at %s (UTC/EVE) from %s to %s ==" % (utctime, sender, to)
 		return result
 
 	def broadcast(self, sender, to, message, servers):

File pizza_auth/authutils.py

-from flask import current_app, redirect, flash
+from flask import current_app, redirect, flash, request, abort
 from flask.ext.login import current_user
 from functools import wraps
 
+
+def groups_required(filter_function):
+	def real_decorator(func):
+		"Decorates a function to require a certain auth group to continue"
+		@wraps(func)
+		def decorated_view(*args, **kwargs):
+			if len(filter(filter_function, current_user.authGroup))==0:
+				flash("You must be in one of the correct groups to access that.", "danger")
+				return redirect("/")
+			else:
+				return func(*args, **kwargs)
+		return decorated_view
+	return real_decorator
+
 def group_required(group):
 	def real_decorator(func):
 		"Decorates a function to require a certain auth group to continue"
 		return decorated_view
 	return real_decorator
 
+def api_key_required(func):
+	@wraps(func)
+	def decorated_view(*args, **kwargs):
+		key = request.args.get('key','')
+		if key not in current_app.config["apikeys"]:
+			abort(401)
+		else:
+			return func(*args, **kwargs)
+	return decorated_view

File pizza_auth/buildgroups.py

+#!/usr/bin/python
+import ts3tools, announce
+from ldaptools import LDAPTools
+from keytools import KeyTools
+import json
+import logging
+import time
+from logging import handlers
+from ldap import MOD_ADD, MOD_DELETE, MOD_REPLACE
+
+# Load configuration
+with open("config.json") as fh:
+	config=json.loads(fh.read())
+assert(config)
+
+# Set up all classes
+ldaptools = LDAPTools(config)
+
+
+if __name__ == "__main__":
+	logger = logging.getLogger("buildgroups")
+	logger.setLevel(logging.DEBUG)
+	fh = logging.FileHandler("./logs/buildgroups_%d.log" % time.time())
+	formatter = logging.Formatter('%(asctime)s - %(message)s')
+	fh.setFormatter(formatter)
+	logger.addHandler(fh)
+
+	allgroups = config["groups"]["opengroups"] + config["groups"]["closedgroups"]
+	for group in allgroups:
+			members = ldaptools.getusers("authGroup=%s" % group)
+			if not members:
+						continue
+			attrs = {}
+			attrs["cn"] = str(group)
+			attrs["description"] = str(("Autogenerated %s group") % group)
+			attrs["member"] = map(lambda x: str(("uid=%s,%s") % (x.uid[0], config["ldap"]["memberdn"])), members)
+ 			try:
+ 		 				ldaptools.deletegroup(group)
+ 			except:
+ 		 				pass
+ 			finally:
+ 		 				ldaptools.addgroup(attrs)

File pizza_auth/emailtools.py

-from jinja2 import FileSystemLoader
+from jinja2 import PackageLoader
 from jinja2.environment import Environment
 import smtplib
 from email.mime.text import MIMEText
 import time
 import json
+import os
 
 class EmailTools():
 
-	def __init__(self, config):
+	def __init__(self, config, loader):
 		self.config = config
 		self.env = Environment()
-		self.env.loader = FileSystemLoader('templates/email')
+		self.env.loader = loader
 
 	def send_email(self, to, subject, body):
 		msg = MIMEText(body)
 		s.quit()
 
 	def render_email(self, to, subject, template, **kwargs):
-		template = self.env.get_template(template)
+		template = self.env.get_template("email/"+template)
 		print template.render(**kwargs)
 		self.send_email(to, subject, template.render(**kwargs))
 

File pizza_auth/keytools.py

 		self.authconfig = config
 		self.bluealliances = self.getBlueAlliances()
 
+	def getapi(self, user):
+		api = eveapi.EVEAPIConnection()
+		auth = api.auth(keyID=user.keyID, vCode=user.vCode)
+		return auth
+
 	def getBlueAlliances(self):
 		standingsapi = eveapi.EVEAPIConnection()
 		auth = standingsapi.auth(keyID=self.config["executorkeyid"], vCode=self.config["executorkeyvcode"])
 
 	def getCharacterStanding(self, character):
 		if character.allianceName == self.authconfig["auth"]["alliance"]:
-			return "PIZZA"
+			return "Internal"
 		elif character.allianceID in self.bluealliances:
 			return "Ally"
 		else:

File pizza_auth/ldaptools.py

 
 class ServerDownException(Exception): pass
 
-class User(UserMixin):
-
-	def __init__(self, attr, domain):
-		self.__dict__.update(attr)
-		self.domain = domain
-
-	def get_id(self):
-		return self.uid[0]
-
-	def get_authgroups(self):
-		if not hasattr(self, "authGroup"):
-			return []
-		else:
-			return filter(lambda x:not x.endswith("-pending"), self.authGroup)
-
-	def get_pending_authgroups(self):
-		if not hasattr(self, "authGroup"):
-			return []
-		else:
-			results = filter(lambda x:x.endswith("-pending"), self.authGroup)
-			return map(lambda x:x[:-8], results)
-
-	def get_jid(self):
-		domains = {
-			"PIZZA": self.domain,
-			"Ally": "allies." + self.domain,
-			"Ineligible": "public." + self.domain
-		}
-		return "%s@%s" % (self.uid[0], domains[self.accountStatus[0]])
-
-	def get_ts3ids(self):
-		if hasattr(self, "ts3uid"):
-			return self.ts3uid
-		else:
-			return []
-
 class LDAPTools():
 	def __init__(self, config):
 		self.authconfig = config
 		l.add_s(dn, ldif)
 		l.unbind_s()
 
+	def deletegroup(self, group):
+		l = ldap.initialize(self.config["server"])
+		l.simple_bind(self.config["admin"], self.config["password"])
+		dn = "cn=%s,%s" % (str(group), self.config["groupdn"])
+		l.delete_s(dn)
+		l.unbind_s()
+		return True
+
 	def modts3id(self, uid, change, ts3id):
 		l = ldap.initialize(self.config["server"])
 		l.simple_bind(self.config["admin"], self.config["password"])
 		l.unbind_s()
 		return True
 
+	def updateattrs(self, uid, change, av_dict):
+		l = ldap.initialize(self.config["server"])
+		l.simple_bind(self.config["admin"], self.config["password"])
+		dn = "uid=%s,%s" % (uid, self.config["memberdn"])
+		l.modify_s(dn, [(change, str(k), str(v)) for k,v in av_dict.items()])
+		return True
+
 	def deleteuser(self, uid):
 		l = ldap.initialize(self.config["server"])
 		l.simple_bind(self.config["admin"], self.config["password"])
 		if data:
 			dn, attrs = data[0]
 			l.unbind_s()
-			return User(attrs, self.authconfig["auth"]["domain"])
+			return self.User(attrs, self.authconfig["auth"]["domain"])
 		l.unbind_s()
 		return None
 
 			else:
 				if result_type == ldap.RES_SEARCH_ENTRY:
 					results.append(result_data[0][1])
-		return map(lambda x:User(x, self.authconfig["auth"]["domain"]), results)
+		return map(lambda x:self.User(x, self.authconfig["auth"]["domain"]), results)
 
+	class User(UserMixin):
 
+		def __init__(self, attr, domain):
+			self.__dict__.update(attr)
+			self.domain = domain
+
+		def get_id(self):
+			return self.uid[0]
+
+		def get_authgroups(self):
+			if not hasattr(self, "authGroup"):
+				return []
+			else:
+				return filter(lambda x:not x.endswith("-pending"), self.authGroup)
+
+		def get_pending_authgroups(self):
+			if not hasattr(self, "authGroup"):
+				return []
+			else:
+				results = filter(lambda x:x.endswith("-pending"), self.authGroup)
+				return map(lambda x:x[:-8], results)
+
+		def get_jid(self):
+			domains = {
+				"Internal": self.domain,
+				"Ally": "allies." + self.domain,
+				"Ineligible": "public." + self.domain
+			}
+			return "%s@%s" % (self.uid[0], domains[self.accountStatus[0]])
+
+		def get_ts3ids(self):
+			if hasattr(self, "ts3uid"):
+				return self.ts3uid
+			else:
+				return []
+
+		def is_admin(self):
+			return bool(filter(lambda x:x.startswith("admin"), self.get_authgroups()))
+
+		def can_ping(self):
+			return bool(filter(lambda x:x.startswith("ping"), self.get_authgroups()))

File pizza_auth/main.py

 from ldaptools import LDAPTools
 from keytools import KeyTools
 from emailtools import EmailTools
-from authutils import group_required
+from reddittools import RedditTools
+from authutils import groups_required, group_required, api_key_required
 from collections import namedtuple
 from ldap import ALREADY_EXISTS
 from ldap import MOD_ADD, MOD_DELETE, MOD_REPLACE
 import string, random
+import redis_wrap
+from updateaccounts import update_characters
 
 app = Flask(__name__)
 
 ts3manager = ts3tools.ts3manager(app.config)
 ldaptools = LDAPTools(app.config)
 keytools = KeyTools(app.config)
-emailtools = EmailTools(app.config)
+emailtools = EmailTools(app.config, app.jinja_loader)
+
+if "reddit" in app.config:
+	reddittools = RedditTools(app.config,ldaptools)
+else:
+	reddittools = None
 
 @login_manager.user_loader
 def load_user(userid):
 @app.route("/login", methods=["POST", "GET"])
 def login():
 	if request.method=="GET":
-		return render_template("login.html")
+		return render_template("login.html", next_page=request.args.get("next"))
 	username = request.form["username"]
 	password = request.form["password"]
 	next_page = request.form["next_page"]
 		return redirect("/login")
 login_manager.login_view = "/login"
 
-recoverymap = {}
+recoverymap = redis_wrap.get_hash("recovery")
 
 @app.route("/forgot_password", methods=["POST", "GET"])
 def forgot_password():
 		recoverymap[token] = username
 		emailtools.render_email(email, "Password Recovery", "forgot_password.txt", url=url, config=app.config)
 		flash("Email sent to "+email, "success")
-		print recoverymap
 	except Exception as e:
-		print e
 		flash("Username/Email mismatch", "danger")
 	return redirect("/login")
 
 		user = ldaptools.getuser(recoverymap[token])
 		login_user(user)
 		del recoverymap[token]
-		print recoverymap
 		flash("Logged in as %s using recovery token." % user.get_id(), "success")
-		return redirect("/account")
+		return render_template("account_reset.html")
 
 @app.route("/logout")
 @login_required
 @app.route("/account")
 @login_required
 def account():
-	return render_template("account.html")
+	reddit_flag = "reddit" in app.config
+	return render_template("account.html", reddit_flag=reddit_flag)
 
 @app.route("/account/update", methods=['POST'])
 @login_required
 def update_account():
 	email = request.form["email"]
-	password = request.form["password"]
+	oldpassword = request.form["oldpassword"]
+	api_id = request.form["api_id"]
+	api_key = request.form["api_key"]
+	update_needed = False
+	if api_id != current_user.keyID[0] or api_key != current_user.vCode[0]:
+		update_needed = True
+	if not ldaptools.check_credentials(current_user.get_id(), oldpassword):
+		flash("You must confirm your old password to update your account.", "danger")
+		return redirect("/account")
 	try:
+		if all(x in request.form for x in ["password", "password_confirm", "oldpassword"]):
+			if request.form["password"] != request.form["password_confirm"]:
+				flash("Password confirmation mismatch.", "danger")
+				return redirect("/account")
+			result = ldaptools.modattr(current_user.get_id(), MOD_REPLACE, "userPassword", ldaptools.makeSecret(request.form["password"]))
+			assert(result)
 		result = ldaptools.modattr(current_user.get_id(), MOD_REPLACE, "email", email)
 		assert(result)
-		result = ldaptools.modattr(current_user.get_id(), MOD_REPLACE, "userPassword", ldaptools.makeSecret(password))
+		result = ldaptools.modattr(current_user.get_id(), MOD_REPLACE, "keyID", api_id)
+		assert(result)
+		result = ldaptools.modattr(current_user.get_id(), MOD_REPLACE, "vCode", api_key)
 		assert(result)
 		flash("Account updated", "success")
 	except Exception:
 		flash("Update failed", "danger")
+	if update_needed is True:
+		update_characters([current_user.get_id()])
+	app.logger.info('User account {0} infos changed'.format(current_user.get_id()))
 	return redirect("/account")
 
 @app.route("/groups")
 	else:
 		return render_template("groups.html", closed_groups=filter(notyours, app.config["groups"]["closedgroups"]), open_groups=filter(notyours, app.config["groups"]["opengroups"]))
 
+
+@app.route("/admin", methods=["GET", "POST"])
+@login_required
+@group_required("admin")
+def admin():
+	user = None
+	if "userid" in request.args:
+		user = ldaptools.getuser(request.args["userid"])
+		if not user:
+			flash("Cannot find that user", "danger")
+		else:
+			user = user.__dict__
+	return render_template("admintools.html", user=user)
+
+@app.route("/admin/deleteuser", methods=["POST"])
+@login_required
+@group_required("admin")
+def deleteuser():
+	user = request.form["userid"]
+	ldaptools.deleteuser(user)
+	flash("Successful delete for %s" % user, "success")
+	return redirect("/admin")
+
+@app.route("/admin/updateuser", methods=["POST"])
+@login_required
+@group_required("admin")
+def updateuser():
+	user = request.form["userid"]
+	update_characters([user])
+	flash("Successful update for %s" % user, "success")
+	return redirect("/admin")
+
 @app.route("/groups/admin")
 @login_required
-@group_required("admin")
+@groups_required(lambda x:x.startswith("admin"))
 def groupadmin():
+	if "admin" in current_user.authGroup:
+		groups = groups=app.config["groups"]["closedgroups"]+app.config["groups"]["opengroups"]
+	else:
+		groups = map(lambda x:x[6:], filter(lambda x:x.startswith("admin-"), current_user.authGroup))
 	pendingusers = ldaptools.getusers("authGroup=*-pending")
 	applications = []
 	for user in pendingusers:
 		for group in user.get_pending_authgroups():
-			applications.append((user.get_id(), group))
-	return render_template("groupsadmin.html", applications=applications, groups=app.config["groups"]["closedgroups"]+app.config["groups"]["opengroups"])
+			if group in groups:
+				applications.append((user.get_id(), group))
+	return render_template("groupsadmin.html", applications=applications, groups=groups)
 
 @app.route("/groups/list/<group>")
 @login_required
-@group_required("admin")
+@groups_required(lambda x:x.startswith("admin"))
 def grouplist(group):
 	users = ldaptools.getusers("authGroup="+group)
 	return render_template("groupmembers.html", group=group, members=users)
 
 @app.route("/groups/admin/approve/<id>/<group>")
 @login_required
-@group_required("admin")
+@groups_required(lambda x:x.startswith("admin"))
 def groupapprove(id, group):
+	if ("admin" not in current_user.get_authgroups()) and ("admin-%s" % group not in current_user.get_authgroups()):
+		flash("You do not have the right to do that.", "danger")
+		return redirect("/groups/admin")
 	try:
 		id = str(id)
 		group = str(group)
 
 @app.route("/groups/admin/deny/<id>/<group>")
 @login_required
-@group_required("admin")
+@groups_required(lambda x:x.startswith("admin"))
 def groupdeny(id, group):
+	if ("admin" not in current_user.get_authgroups()) and ("admin-%s" % group not in current_user.get_authgroups()):
+		flash("You do not have the right to do that.", "danger")
+		return redirect("/groups/admin")
 	try:
 		id = str(id)
 		group = str(group)
 
 @app.route("/groups/admin/remove/<id>/<group>")
 @login_required
-@group_required("admin")
+@groups_required(lambda x:x.startswith("admin"))
 def groupremove(id, group):
+	if ("admin" not in current_user.get_authgroups()) and ("admin-%s" % group not in current_user.get_authgroups()):
+		flash("You do not have the right to do that.", "danger")
+		return redirect("/groups/admin")
 	id = str(id)
 	group = str(group)
 	ldaptools.modgroup(id, MOD_DELETE, group)
 	return redirect("/groups/list/"+group)
 
 
+@app.route("/groups/admin/admin/<id>/<group>")
+@login_required
+@groups_required(lambda x:x.startswith("admin"))
+def groupmkadmin(id, group):
+	if ("admin" not in current_user.get_authgroups()) and ("admin-%s" % group not in current_user.get_authgroups()):
+		flash("You do not have the right to do that.", "danger")
+		return redirect("/groups/admin")
+	id = str(id)
+	group = str(group)
+	try:
+		ldaptools.modgroup(id, MOD_ADD, "admin-%s" % group)
+		flash("Membership of admin-%s added for %s" % (group, id), "success")
+	except:
+		flash("That user is already in that group.", "danger")
+	return redirect("/groups/list/"+group)
+
+@app.route("/groups/admin/ping/<id>/<group>")
+@login_required
+@groups_required(lambda x:x.startswith("admin"))
+def groupmkping(id, group):
+	if ("admin" not in current_user.get_authgroups()) and ("admin-%s" % group not in current_user.get_authgroups()):
+		flash("You do not have the right to do that.", "danger")
+		return redirect("/groups/admin")
+	id = str(id)
+	group = str(group)
+	try:
+		ldaptools.modgroup(id, MOD_ADD, "ping-%s" % group)
+		flash("Membership of ping-%s added for %s" % (group, id), "success")
+	except:
+		flash("That user is already in that group.", "danger")
+	return redirect("/groups/list/"+group)
+
+
+
+
 
 @app.route("/groups/apply/<group>")
 @login_required
 	if group in app.config["groups"]["closedgroups"]:
 		group = group+"-pending"
 		join = False
-	print current_user.accountStatus
 	if current_user.accountStatus[0]=="Ineligible":
 		if group not in app.config["groups"]["publicgroups"]:
 			flash("You cannot join that group.", "danger")
 
 @app.route("/ping")
 @login_required
-@group_required("ping")
+@groups_required(lambda x:x.startswith("ping"))
 def ping():
 	return render_template("ping.html")
 
 
 @app.route("/ping/group", methods=["POST"])
 @login_required
-@group_required("ping")
+@groups_required(lambda x:x.startswith("ping"))
 def ping_send_group():
+	if ("ping" not in current_user.get_authgroups()) and ("ping-%s" % request.form["group"] not in current_user.get_authgroups()):
+		flash("You do not have the right to do that.", "danger")
+		return redirect("/ping")
 	count = pingbot.groupbroadcast(current_user.get_id(), "(|(authGroup={0})(corporation={0})(alliance={0}))".format(request.form["group"]), request.form["message"], request.form["group"])
 	flash("Broadcast sent to %d members in %s" % (count, request.form["group"]), "success")
 	return redirect("/ping")
 @login_required
 def add_tss3id():
 	ts3id = str(request.form["ts3id"])
-	print "trying to auth",ts3id
-	print "account is", current_user.accountStatus[0]
 	ts3group = {
-			"PIZZA": app.config["ts3"]["servergroups"]["full"],
+			"Internal": app.config["ts3"]["servergroups"]["full"],
 			"Ally": app.config["ts3"]["servergroups"]["ally"],
 			"Ineligible": app.config["ts3"]["servergroups"]["none"]
 			}
 def refresh_ts3id():
 	ts3ids = current_user.ts3uid
 	ts3group = {
-			"PIZZA": app.config["ts3"]["servergroups"]["full"],
+			"Internal": app.config["ts3"]["servergroups"]["full"],
 			"Ally": app.config["ts3"]["servergroups"]["ally"],
 			"Ineligible": app.config["ts3"]["servergroups"]["none"]
 			}
 @app.route('/')
 def index():
 	if current_user.is_anonymous():
-		next_page = request.args.get('next')
-		return render_template("index.html", next_page=next_page)
+		return render_template("index.html")
 	else:
 		return render_template("index_user.html")
 
 		session["chars"] = json.dumps(chars, default=lambda x:x.__dict__)
 		return render_template("characters.html", characters=chars)
 	except Exception as e:
-		print e
-		raise
 		flash("Invalid API key", "danger")
 		return redirect(url_for("index"))
 
 	results = []
 	for char in characters:
 		r = {}
-		print char
 		for col, row in zip(char["_cols"], char["_row"]):
 			r[col] = row
 		r["result"] = char["result"]
 	results = filter(lambda x:x.lower().startswith(term.lower()), entities+app.config["groups"]["closedgroups"]+app.config["groups"]["opengroups"])
 	return json.dumps(results)
 
+@app.route("/account/reddit")
+@login_required
+def reddit():
+	redirect_uri = app.config["reddit"]["redirect_base"] + url_for('reddit_loop')
+	if hasattr(current_user, "redditAccount"):
+		flash("Already registered with reddit: %s" % (current_user.redditName), "error")
+		return redirect(url_for('services'))
+	r = reddittools.get_reddit_client(redirect_uri)
+	url = r.get_authorize_url(app.config["reddit"]["statekey"], 'identity', False)
+	return redirect(url)
+
+@app.route("/account/reddit/loop")
+@login_required
+def reddit_loop():
+	query = request.args
+	result = reddittools.verify_token(
+			current_user.get_id(),
+			query)
+
+	if result:
+		user = load_user(current_user.get_id())
+		flash("Successfully updated or added reddit account: %s." % (user.redditName[0],), "success")
+	else:
+		flash("Failed to update reddit account.", "danger")
+
+	return redirect(url_for("index"))
+
+@app.route("/apiv1/group/<string:group>")
+@api_key_required
+def groupdump(group):
+	allusers = ldaptools.getusers("authGroup=%s" % group)
+	results = map(lambda x:x.characterName[0], allusers)
+	return json.dumps(results)
+
 @app.teardown_appcontext
 def shutdown_session(exception=None):
 	pass

File pizza_auth/reddittools.py

+#!/usr/bin/env python
+
+import praw
+from flask import url_for
+from hashlib import sha1
+
+class RedditTools():
+	def __init__(self, config, ldaptools):
+		self.authconfig = config
+		self.config = config["reddit"]
+                self.ldaptools = ldaptools
+
+                self.ldaptools.User.get_reddit_token = lambda x:hasattr(x, "redditToken") and x.redditToken[0] or None
+                self.ldaptools.User.get_reddit_name  = lambda x:hasattr(x, "redditName") and x.redditName[0] or None
+
+        def get_reddit_client(self, redirect_uri):
+            r = praw.Reddit(
+                    self.config["clientname"]
+                    )
+
+            r.set_oauth_app_info(
+                    client_id = self.config["clientid"],
+                    client_secret = self.config["clientsecret"],
+                    redirect_uri = redirect_uri
+                    )
+
+            return r
+
+        def verify_token(self, uid, query_args):
+            code = query_args.get('code', None)
+            state = query_args.get('state', None)
+            user = self.ldaptools.getuser(uid)
+
+            if code and state:
+                state_key = self.config["statekey"]
+                if state_key == state:
+                    r = self.get_reddit_client(self.config["redirect_base"] + url_for('reddit_loop'))
+                    access_info = r.get_access_information(code)
+                    auth_reddit = r.get_me()
+                    if 'redditAccount' in user.objectClass:
+                        if hasattr(user, 'redditName') and hasattr(user, 'redditToken'):
+                            from ldap import MOD_REPLACE
+                            self.ldaptools.updateattrs(uid, MOD_REPLACE, {
+                                'redditName': auth_reddit.name,
+                                'redditToken': access_info['access_token']
+                                })
+                        else:
+                            # Something went horribly wrong.
+                            return False
+                    else:
+                        from ldap import MOD_ADD
+                        self.ldaptools.updateattrs(uid, MOD_ADD, {
+                            'objectClass': 'redditAccount',
+                            'redditName': auth_reddit.name,
+                            'redditToken': access_info['access_token']
+                            })
+
+                    return True
+                    
+            return False
+
+        def get_auth_url(self):
+            state_key = self.config["statekey"]
+            redirect_uri = "http://newauth.talkinlocal.org" + url_for('reddit_loop')
+            r = self.get_reddit_client(redirect_uri)
+            url = r.get_authorize_url(state_key, 'identity', False)
+
+            return url

File pizza_auth/run.py

 from main import app
-app.run(host='127.0.0.1', port=8090, debug=True)
+app.run(host='0.0.0.0', port=8090, debug=True)

File pizza_auth/skills.py

File contents unchanged.

File pizza_auth/templates/account.html

 {% block header %}Account Management{% endblock %}
 {% block body %}
 <div class="row">
+	{% if reddit_flag %}
+	<div class="col-md-4">
+		<div class="panel panel-primary">
+			<div class="panel-heading"><h3 class="panel-title">Reddit Verification</h3></div>
+			<div class="panel-body">
+			<p>Reddit Account: <span class="text-info">{{ current_user.get_reddit_name() }}</span></p>
+			<p><a href="{{ url_for("reddit") }}" class="btn btn-default">Update or Verify</a></p>
+			</div>
+		</div>
+	</div>
+	{% endif %}
 	<div class="col-md-4">
 		<div class="panel panel-primary">
 			<div class="panel-heading"><h3 class="panel-title">Your Account</h3></div>
 						<tr>
 							<th>Confirm New Password</th><td><input type="password" name="password_confirm" id="password_confirm" /></td>
 						</tr>
+						<tr>
+							<th>Confirm Old Password</th><td><input type="password" name="oldpassword" id="oldpassword" /></td>
+						</tr>
+						<tr>
+							<th>Key ID</th><td><input name="api_id" value="{{ current_user.keyID[0] }}" required/></td>
+						</tr>
+						<tr>
+							<th>vCode</th><td><input name="api_key" value="{{ current_user.vCode[0] }}" required/></td>
+						</tr>
 					</table>
 				<button type="submit" class="btn btn-default">Update</button>
 				</form>
 							email: {
 								required: true,
 								email: true
-							},
-							password: "required",
-							password_confirm: {
-								equalTo: "#password"
 							}
 						}
 					});

File pizza_auth/templates/account_reset.html

+{% extends "base.html" %}
+{% block title %}Account Management{% endblock %}
+{% block header %}Account Management{% endblock %}
+{% block body %}
+<div class="row">
+	<div class="col-md-4">
+		<div class="panel panel-primary">
+			<div class="panel-heading"><h3 class="panel-title">Your Account</h3></div>
+			<div class="panel-body">
+				<form role="form" id="update_account" action="/account/update" method="post">
+					<table class="table table-striped">
+						<tr>
+							<th>Account</th><td>{{ current_user.get_id() }}</td>
+						</tr>
+						<tr>
+							<th>Main Character</th><td>{{ current_user.characterName[0] }}</td>
+						</tr>
+						<tr>
+							<th>Email</th><td><input name="email" value="{{ current_user.email[0] }}" required/></td>
+						</tr>
+						<tr>
+							<th>New Password</th><td><input type="password" name="password" id="password" /></td>
+						</tr>
+						<tr>
+							<th>Confirm New Password</th><td><input type="password" name="password_confirm" id="password_confirm" /></td>
+						</tr>
+					</table>
+				<button type="submit" class="btn btn-default">Update</button>
+				</form>
+				<script src="http://jquery.bassistance.de/validate/jquery.validate.js"></script>
+				<script src="http://jquery.bassistance.de/validate/additional-methods.js"></script>
+				<script>
+					$( "#update_account" ).validate({
+						rules: {
+							email: {
+								required: true,
+								email: true
+							},
+							password: "required",
+							password_confirm: {
+								equalTo: "#password"
+							}
+						}
+					});
+				</script>
+
+
+			</div>
+		</div>
+	</div>
+</div class="row">
+{% endblock %}

File pizza_auth/templates/admintools.html

+{% extends "base.html" %}
+{% block title %}Admin{% endblock %}
+{% block header %}Admin{% endblock %}
+{% block body %}
+	<div class="col-md-{{ user and 8 or 4 }}">
+		<div class="panel panel-primary">
+			<div class="panel-heading"><h3 class="panel-title">Dump User</h3></div>
+			<div class="panel-body">
+				<form action="/admin">
+					<label for="userid">User ID</label><br /><input class="form-control" rows=6 style="width:100%" type="text" name="userid"></input>
+					<div class="controls">
+						<button type="submit" class="btn btn-primary" value="submit" />Submit</button>
+					</div>
+				</form>
+				{% if user %}
+					<table class="table table-bordered">
+					{% for key, value in user.iteritems() %}
+						<tr>
+							<th> {{ key }} </th>
+							<td> {{ value|length==1 and value[0] or value }} </td>
+						</tr>
+					{% endfor %}
+					</table>
+				{% endif %}
+			</div>
+		</div>
+	</div>
+
+	<div class="col-md-4">
+		<div class="panel panel-primary">
+			<div class="panel-heading"><h3 class="panel-title">Delete User</h3></div>
+			<div class="panel-body">
+				<form action="/admin/deleteuser" method="post">
+					<label for="userid">User ID</label><br /><input class="form-control" rows=6 style="width:100%" type="text" name="userid"></input>
+					<div class="controls">
+						<button type="submit" class="btn btn-danger" value="submit" />Delete</button>
+					</div>
+				</form>
+			</div>
+		</div>
+	</div>
+	<div class="col-md-4">
+		<div class="panel panel-primary">
+			<div class="panel-heading"><h3 class="panel-title">Force update User</h3></div>
+			<div class="panel-body">
+				<form action="/admin/updateuser" method="post">
+					<label for="userid">User ID</label><br /><input class="form-control" rows=6 style="width:100%" type="text" name="userid"></input>
+					<div class="controls">
+						<br>
+						<button type="submit" class="btn btn-warning" value="submit" />Update</button>
+					</div>
+				</form>
+			</div>
+		</div>
+	</div>
+{% endblock %}

File pizza_auth/templates/base.html

 					<li><a href="/services">Services</a></li>
 					<li><a href="/groups">Groups</a></li>
 
-				{% if "admin" in current_user.get_authgroups() %}
+				{% if current_user.is_admin() %}
 					<li><a href="/groups/admin">Group Admin</a></li>
+					<li><a href="/admin">Admin</a></li>
 				{% endif %}
-				{% if "ping" in current_user.get_authgroups() %}
+				{% if current_user.can_ping() %}
 					<li><a href="/ping">Pings</a></li>
 				{% endif %}
 				{% if "timerboard" in current_user.get_authgroups() %}
-					<li><a href="http://timers.xxpizzaxx.com/admin">Timerboard</a></li>
+					<li><a href="http://timers.{{ config["auth"]["domain"] }}/admin">Timerboard</a></li>
 				{% else %}
-					<li><a href="http://timers.xxpizzaxx.com/">Timerboard</a></li>
+					<li><a href="http://timers.{{ config["auth"]["domain"] }}/">Timerboard</a></li>
 				{% endif %}
 				</ul>
 

File pizza_auth/templates/groupmembers.html

 				<td>{{ user.characterName[0] }}</td>
 				<td>{{ user.corporation[0] }}</td>
 				<td>{% if user.alliance is defined %}{{ user.alliance[0] }}{% endif %}</td>
-				<td><a class="btn btn-xs btn-danger pull-right" href="/groups/admin/remove/{{ user.get_id() }}/{{ group }}">Remove</a></td>
+				<td>
+					<div class="btn-group pull-right">
+						<a class="btn btn-xs btn-success" href="/groups/admin/admin/{{ user.get_id() }}/{{ group }}">Admin</a>
+						<a class="btn btn-xs btn-info" href="/groups/admin/ping/{{ user.get_id() }}/{{ group }}">Ping</a>
+						<a class="btn btn-xs btn-danger" href="/groups/admin/remove/{{ user.get_id() }}/{{ group }}">Remove</a>
+					</div>
+				</td>
 			</tr>
 			{% endfor %}
 		</table>

File pizza_auth/templates/index_user.html

 	<div class="carousel-inner">
 		<div class="item active">
 			<div class="inner-item">
-				<a href="http://timers.xxpizzaxx.com">
+				<a href="http://timers.{{ config["auth"]["domain"] }}">
 					<img src="static/img/timerboard.png" data-src="holder.js/900x350/auto/#555:#333/text:timerboard" alt="timerboard logo">
 				</a>
 			</div>
 		</div>
 		<div class="item">
 			<div class="inner-item">
-				<a href="http://wiki.xxpizzaxx.com">
+				<a href="http://wiki.{{ config["auth"]["domain"] }}">
 					<img src="static/img/wiki.png" data-src="holder.js/900x350/auto/#555:#333/text:timerboard" alt="pizza wiki logo">
 				</a>
 			</div>

File pizza_auth/templates/ping.html

 {% block header %}Ping Tool{% endblock %}
 {% block body %}
 <div class="row">
+	{% if "ping" in current_user.get_authgroups() %}
 	<div class="col-md-4">
 		<div class="panel panel-primary">
 		<div class="panel-heading"><h3 class="panel-title">Server-wide Ping</h3></div>
 		</form>
 		</div>
 	</div>
+	{% endif %}
 
 	<div class="col-md-4">
 		<div class="panel panel-primary">
 		</form>
 		</div>
 	</div>
+
+	{% if "ping" in current_user.get_authgroups() %}
 	<div class="col-md-4">
 		<div class="panel panel-primary">
 		<div class="panel-heading"><h3 class="panel-title">Advanced Group Ping</h3></div>
 			</form>
 		</div>
 	</div>
+	{% endif %}
 </div class="row">
 
 <script>

File pizza_auth/updateaccounts.py

 import logging
 import time
 from logging import handlers
-from ldap import MOD_ADD, MOD_DELETE, MOD_REPLACE
+from ldap import MOD_ADD, MOD_DELETE, MOD_REPLACE, TYPE_OR_VALUE_EXISTS
 
 # Load configuration
 with open("config.json") as fh:
 
 safecharacters = ["twistedbot", "pingbot", "root", "deszra", "dimethus", "webchat"]
 
-if __name__ == "__main__":
+def update_characters(characters=None):
 	logger = logging.getLogger("updateusers")
 	logger.setLevel(logging.DEBUG)
 	fh = logging.FileHandler("./logs/updateusers_%d.log" % time.time())
 	fh.setFormatter(formatter)
 	logger.addHandler(fh)
 
-	for character in ldaptools.getusers("objectclass=xxPilot"):
+	ldap_characters = []
+	if characters is not None:
+		for character in characters:
+			ldap_characters.append(ldaptools.getuser(character))
+	else:
+		ldap_characters = ldaptools.getusers("objectclass=xxPilot")
+
+	for character in ldap_characters:
 		try:
 			characters = keytools.getcharacters(character.keyID, character.vCode)
 			characters = json.dumps(characters, default=lambda x:x.__dict__)
 			if character.alliance[0] != newcharacter["allianceName"]:
 				logger.info( "%s alliance update \t %s -> %s" % ( character.get_id(), character.alliance[0], newcharacter["allianceName"]) )
 				if create:
-					ldaptools.modattr(character.get_id(), MOD_ADD, "alliance", newcharacter["allianceName"])
+					try:
+						ldaptools.modattr(character.get_id(), MOD_ADD, "alliance", newcharacter["allianceName"])
+
+					except TYPE_OR_VALUE_EXISTS:
+						# Sneaky devil
+						# alliances can change
+						ldaptools.modattr(character.get_id(), MOD_REPLACE, "alliance", newcharacter["allianceName"])
 				else:
 					ldaptools.modattr(character.get_id(), MOD_REPLACE, "alliance", newcharacter["allianceName"])
 			if character.corporation[0] != newcharacter["corporationName"]:
 				ldaptools.modattr(character.get_id(), MOD_REPLACE, "accountStatus", "Expired")
 		except AssertionError:
 			logger.error("%s is not on this account" % character.characterName[0])
+
+
+if __name__ == "__main__":
+	update_characters()

File requirements.txt

 flask-login
 python-ldap
 ts3
+redis_wrap
 		'flask-login',
 		'python-ldap',
 		'python-ts3',
+		'redis_wrap',
 	]
 )
 
+To Do List
+==========
+
+* Granular group admins
+* Granular ping groups
+* Adding to "invisible" groups
+* Multi-character auth
+* TwistedBot integration with pings
+* TwistedBot integration with groups