Merge branch 'master' of ssh://git.crans.org/git/cranspasswords

This commit is contained in:
Vincent Le Gallic 2013-03-07 17:25:05 +01:00
commit 57ff241359
6 changed files with 216 additions and 17 deletions

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
clientconfig.py clientconfig.py
serverconfig.py serverconfig.py
*.pyc *.pyc
# Dossier contenant les mots de passe
db

1
README
View file

@ -7,5 +7,6 @@ ordinateur) et le serveur.
$ git clone git://git.crans.org/git/cranspasswords.git $ git clone git://git.crans.org/git/cranspasswords.git
* Copier clientconfig.example.py en clientconfig.py et adapter * Copier clientconfig.example.py en clientconfig.py et adapter
à vos besoins à vos besoins
* Si ce n'est déjà fait, indiquer votre clé publique sur gest_crans
* ??? * ???

View file

@ -9,6 +9,12 @@ servers = {
'/root/cranspasswords/server'], '/root/cranspasswords/server'],
'user' : os.getenv('USER') # À définir à la main pour les personnes 'user' : os.getenv('USER') # À définir à la main pour les personnes
# n'ayant pas le même login sur leur pc # n'ayant pas le même login sur leur pc
},
'ovh': {
'server_cmd': ['/usr/bin/ssh', 'ovh.crans.org',\
'/root/cranspasswords/server'],
'user' : os.getenv('USER') # À définir à la main pour les personnes
# n'ayant pas le même login sur leur pc
} }
} }

View file

@ -10,7 +10,14 @@ import os
import atexit import atexit
import argparse import argparse
import re import re
import random
import string
import datetime
try:
import clientconfig as config import clientconfig as config
except ImportError:
print "Read the README"
sys.exit(1)
## Password pattern in files: ## Password pattern in files:
PASS = re.compile('[\t ]*pass(?:word)?[\t ]*:[\t ]*(.*)\r?\n?$', \ PASS = re.compile('[\t ]*pass(?:word)?[\t ]*:[\t ]*(.*)\r?\n?$', \
@ -29,7 +36,8 @@ GPG_ARGS = {
DEBUG = False DEBUG = False
VERB = False VERB = False
CLIPBOARD = bool(os.getenv('DISPLAY')) # Par défaut, place-t-on le mdp dans le presse-papier ? # Par défaut, place-t-on le mdp dans le presse-papier ?
CLIPBOARD = bool(os.getenv('DISPLAY')) and os.path.exists('/usr/bin/xclip')
FORCED = False #Mode interactif qui demande confirmation FORCED = False #Mode interactif qui demande confirmation
NROLES = None # Droits à définir sur le fichier en édition NROLES = None # Droits à définir sur le fichier en édition
SERVER = None SERVER = None
@ -56,6 +64,18 @@ def gpg(command, args = None):
proc.stderr.close() proc.stderr.close()
return proc.stdin, proc.stdout return proc.stdin, proc.stdout
class simple_memoize(object):
""" Memoization/Lazy """
def __init__(self, f):
self.f = f
self.val = None
def __call__(self):
if self.val==None:
self.val = self.f()
return self.val
###### ######
## Remote commands ## Remote commands
@ -84,14 +104,17 @@ def remote_command(command, arg = None, stdin_contents = None):
sshin.close() sshin.close()
return json.loads(sshout.read()) return json.loads(sshout.read())
@simple_memoize
def all_keys(): def all_keys():
"""Récupère les clés du serveur distant""" """Récupère les clés du serveur distant"""
return remote_command("listkeys") return remote_command("listkeys")
@simple_memoize
def all_roles(): def all_roles():
"""Récupère les roles du serveur distant""" """Récupère les roles du serveur distant"""
return remote_command("listroles") return remote_command("listroles")
@simple_memoize
def all_files(): def all_files():
"""Récupère les fichiers du serveur distant""" """Récupère les fichiers du serveur distant"""
return remote_command("listfiles") return remote_command("listfiles")
@ -108,11 +131,19 @@ def rm_file(filename):
"""Supprime le fichier sur le serveur distant""" """Supprime le fichier sur le serveur distant"""
return remote_command("rmfile", filename) return remote_command("rmfile", filename)
@simple_memoize
def get_my_roles(): def get_my_roles():
"""Retoure la liste des rôles perso""" """Retoure la liste des rôles perso"""
allr = all_roles() allr = all_roles()
return filter(lambda role: SERVER['user'] in allr[role],allr.keys()) return filter(lambda role: SERVER['user'] in allr[role],allr.keys())
def gen_password():
"""Generate random password"""
random.seed(datetime.datetime.now().microsecond)
chars = string.letters + string.digits + '/=+*'
length = 15
return ''.join([random.choice(chars) for _ in xrange(length)])
###### ######
## Local commands ## Local commands
@ -140,17 +171,38 @@ def check_keys():
return True return True
return False return False
def encrypt(roles, contents): def get_recipients_of_roles(roles):
"""Chiffre le contenu pour les roles donnés""" """Renvoie les destinataires d'un rôle"""
recipients = set() recipients = set()
allroles = all_roles() allroles = all_roles()
allkeys = all_keys()
email_recipients = []
for role in roles: for role in roles:
for recipient in allroles[role]: for recipient in allroles[role]:
recipients.add(recipient) recipients.add(recipient)
return recipients
def get_dest_of_roles(roles):
""" Summarize recipients of a role """
allkeys = all_keys()
def additionnal_info(rec):
""" Gives additionnal information for a given recipient """
if len(allkeys[rec]) == 0:
return ""
out = allkeys[rec][0]
if len(allkeys[rec]) > 1:
out += " -> " + allkeys[rec][1]
return "(%s)" % out
return ["%s %s" % (rec, additionnal_info(rec)) for rec in \
get_recipients_of_roles(roles) ]
def encrypt(roles, contents):
"""Chiffre le contenu pour les roles donnés"""
allkeys = all_keys()
recipients = get_recipients_of_roles(roles)
email_recipients = []
for recipient in recipients: for recipient in recipients:
email, key = allkeys[recipient] email, key = allkeys[recipient]
if key: if key:
@ -194,25 +246,39 @@ def get_password(name):
## Interface ## Interface
def editor(texte): def editor(texte, annotations=""):
""" Lance $EDITOR sur texte""" """ Lance $EDITOR sur texte.
f = tempfile.NamedTemporaryFile() Renvoie le nouveau texte si des modifications ont été apportées, ou None
"""
# Avoid syntax hilight with ".txt". Would be nice to have some colorscheme
# for annotations ...
f = tempfile.NamedTemporaryFile(suffix='.txt')
atexit.register(f.close) atexit.register(f.close)
f.write(texte) f.write(texte)
for l in annotations.split('\n'):
f.write("# %s\n" % l.encode('utf-8'))
f.flush() f.flush()
proc = subprocess.Popen(os.getenv('EDITOR') + ' ' + f.name,shell=True) proc = subprocess.Popen(os.getenv('EDITOR') + ' ' + f.name,shell=True)
os.waitpid(proc.pid,0) os.waitpid(proc.pid,0)
f.seek(0) f.seek(0)
ntexte = f.read() ntexte = f.read()
f.close() f.close()
return texte <> ntexte and ntexte or None ntexte = '\n'.join(filter(lambda l: not l.startswith('#'), ntexte.split('\n')))
if texte != ntexte:
return ntexte
return None
def show_files(): def show_files():
proc = subprocess.Popen("cat",stdin=subprocess.PIPE,shell=True) proc = subprocess.Popen("cat",stdin=subprocess.PIPE,shell=True)
out = proc.stdin out = proc.stdin
out.write("""Liste des fichiers disponibles\n""" ) out.write("""Liste des fichiers disponibles\n""" )
my_roles = get_my_roles() my_roles = get_my_roles()
for (fname,froles) in all_files().iteritems(): files = all_files()
keys = files.keys()
keys.sort()
for fname in keys:
froles = files[fname]
access = set(my_roles).intersection(froles) != set([]) access = set(my_roles).intersection(froles) != set([])
out.write(" %s %s (%s)\n" % ((access and '+' or '-'),fname,", ".join(froles))) out.write(" %s %s (%s)\n" % ((access and '+' or '-'),fname,", ".join(froles)))
out.write("""--Mes roles: %s\n""" % \ out.write("""--Mes roles: %s\n""" % \
@ -227,6 +293,11 @@ def show_roles():
if role.endswith('-w'): continue if role.endswith('-w'): continue
print " * " + role print " * " + role
def show_servers():
print """Liste des serveurs disponibles"""
for server in config.servers.keys():
print " * " + server
old_clipboard = None old_clipboard = None
def saveclipboard(restore=False): def saveclipboard(restore=False):
global old_clipboard global old_clipboard
@ -284,12 +355,15 @@ def show_file(fname):
def edit_file(fname): def edit_file(fname):
value = get_file(fname) value = get_file(fname)
nfile = False nfile = False
annotations = u""
if value == False: if value == False:
nfile = True nfile = True
print "Fichier introuvable" print "Fichier introuvable"
if not confirm("Créer fichier ?"): if not confirm("Créer fichier ?"):
return return
texte = "" annotations += u"""Ceci est un fichier initial contenant un mot de passe
aléatoire, pensez à rajouter une ligne "login: ${login}" """
texte = "pass: %s\n" % gen_password()
roles = get_my_roles() roles = get_my_roles()
# Par défaut les roles d'un fichier sont ceux en écriture de son # Par défaut les roles d'un fichier sont ceux en écriture de son
# créateur # créateur
@ -303,7 +377,16 @@ def edit_file(fname):
sin.write(value['contents']) sin.write(value['contents'])
sin.close() sin.close()
texte = sout.read() texte = sout.read()
ntexte = editor(texte) value['roles'] = NROLES or value['roles']
annotations += u"Ce fichier sera chiffré pour les rôles suivants :\n%s\n\
C'est-à-dire pour les utilisateurs suivants :\n%s" % (
', '.join(value['roles']),
'\n'.join(' %s' % rec for rec in get_dest_of_roles(value['roles']))
)
ntexte = editor(texte, annotations)
if ntexte == None and not nfile and NROLES == None: if ntexte == None and not nfile and NROLES == None:
print "Pas de modifications effectuées" print "Pas de modifications effectuées"
else: else:
@ -388,7 +471,7 @@ if __name__ == "__main__":
help="Mode verbeux") help="Mode verbeux")
parser.add_argument('-c','--clipboard',action='store_true',default=None, parser.add_argument('-c','--clipboard',action='store_true',default=None,
help="Stocker le mot de passe dans le presse papier") help="Stocker le mot de passe dans le presse papier")
parser.add_argument('--noclipboard',action='store_false',default=None, parser.add_argument('--no-clip', '--noclip', '--noclipboard',action='store_false',default=None,
dest='clipboard', dest='clipboard',
help="Ne PAS stocker le mot de passe dans le presse papier") help="Ne PAS stocker le mot de passe dans le presse papier")
parser.add_argument('-f','--force',action='store_true',default=False, parser.add_argument('-f','--force',action='store_true',default=False,
@ -417,6 +500,9 @@ if __name__ == "__main__":
action_grp.add_argument('--list-roles',action='store_const',dest='action', action_grp.add_argument('--list-roles',action='store_const',dest='action',
default=show_file,const=show_roles, default=show_file,const=show_roles,
help="Lister les rôles des gens") help="Lister les rôles des gens")
action_grp.add_argument('--list-servers',action='store_const',dest='action',
default=show_file,const=show_servers,
help="Lister les rôles serveurs")
action_grp.add_argument('--recrypt-role',action='store_const',dest='action', action_grp.add_argument('--recrypt-role',action='store_const',dest='action',
default=show_file,const=update_role, default=show_file,const=update_role,
help="Met à jour (reencode les roles)") help="Met à jour (reencode les roles)")

View file

@ -0,0 +1,101 @@
# Fonction de notre auto completion
contain(){
local i
for i in $2; do
if [[ "$i" = "$1" ]]; then
return 0
fi
done
return 1
}
if [[ "$EDITOR" = "" ]]; then
export EDITOR="nano";
fi
_cranspasswords(){
# declaration des variables locales
local argc first last prev cur cur_first_char opts_short opts role_dir pass_dir server server_list role_list pass_list timeout
role_dir="/tmp/cranspasswords-$USER-role/"
pass_dir="/tmp/cranspasswords-$USER-passwords/"
# Combien de temps on garde les réponses du serveur en cache (en minutes)
timeout=5
#COMPREPLY désigne la réponse à renvoyer pour la complétion actuelle
COMPREPLY=()
# argc : vaut le nombre d'argument actuel sur la ligne de commande
argc=${COMP_CWORD};
# cur : désigne la chaine de caractère actuelle pour le dernier mot de la ligne de commande
first="${COMP_WORDS[1]}"
last="${COMP_WORDS[$(($argc - 1 ))]}"
prev="${COMP_WORDS[$(($argc - 2 ))]}"
cur="${COMP_WORDS[argc]}"
cur_first_char=${cur:0:1}
opts_short="-h -v -c -f -l"
opts="--help --server --verbose --clipboard --noclipboard --force --edit --view --remove --list --check-keys --update-keys --list-roles --recrypt-roles --roles --list-servers"
mkdir -p -m 700 "$role_dir"
mkdir -p -m 700 "$pass_dir"
find "$role_dir" -type f -mmin +$timeout -exec rm -f {} \;
find "$pass_dir" -type f -mmin +$timeout -exec rm -f {} \;
# On détermine si on utilsie un serveur alternatif
if contain "--server" "${COMP_WORDS[*]}"; then
if [[ "$prev" = "--server" ]]; then
_cranspasswords_server=$last;
fi
else
_cranspasswords_server="default";
fi
server=$_cranspasswords_server
# les options possibles pour notre auto-complétion
if [[ $cur_first_char = "-" ]]; then
COMPREPLY=( $(compgen -W "$opts" -- $cur ) )
return 0
fi
if [[ "$last" = "--server" ]]; then
server_list="`cranspasswords --list-servers | grep -- "*" | awk '{print $2}'`"
COMPREPLY=( $(compgen -W "$server_list" -- $cur ) )
return 0
fi
if [[ "$last" = "--roles" ]]; then
if [ ! -f "${role_dir}$server" ]; then
echo "`cranspasswords --server $server --list-roles | grep -- "*" | awk '{print $2}'`" > "${role_dir}$server"
fi
role_list="`cat "${role_dir}$server"`"
COMPREPLY=( $(compgen -W "$role_list" -- $cur ) )
return 0
fi
if [[ "$last" = "--edit" ]]; then
if [ ! -f "${pass_dir}${server}-w" ]; then
echo "`cranspasswords --server $server -l | grep "+" | awk '{print $2}'`" > "${pass_dir}${server}-w"
fi
pass_list="`cat "${pass_dir}${server}-w"`"
COMPREPLY=( $(compgen -W "$pass_list" -- $cur ) )
return 0
fi
if true; then
if [ ! -f "${pass_dir}$server" ]; then
echo "`cranspasswords --server $server -l | grep "\( +\| -\)" | awk '{print $2}'`" > "${pass_dir}$server"
fi
pass_list="`cat "${pass_dir}$server"`"
COMPREPLY=( $(compgen -W "$pass_list" -- $cur ) )
return 0
fi
}
# On active l'auto-completion
complete -F _cranspasswords cranspasswords

2
server Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
sudo /root/cranspasswords/server.py $*