generatepassword recipe: added missing O_TRUNC
[slapos.git] / slapos / recipe / generatepassword.py
1 # vim: set et sts=2:
2 ##############################################################################
3 #
4 # Copyright (c) 2012 Vifib SARL and Contributors. All Rights Reserved.
5 #
6 # WARNING: This program as such is intended to be used by professional
7 # programmers who take the whole responsibility of assessing all potential
8 # consequences resulting from its eventual inadequacies and bugs
9 # End users who are looking for a ready-to-use solution with commercial
10 # guarantees and support are strongly adviced to contract a Free Software
11 # Service Company
12 #
13 # This program is Free Software; you can redistribute it and/or
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; either version 3
16 # of the License, or (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 #
27 ##############################################################################
28
29 import errno
30 import os
31 import random
32 import string
33
34 def generatePassword(length):
35 return ''.join(random.SystemRandom().sample(string.ascii_lowercase, length))
36
37
38 class Recipe(object):
39 """Generate a password that is only composed of lowercase letters
40
41 This recipe only makes sure that ${:passwd} does not end up in `.installed`
42 file, which is world-readable by default. So be careful not to spread it
43 throughout the buildout configuration by referencing it directly: see
44 recipes like slapos.recipe.template:jinja2 to safely process the password.
45
46 Options:
47 - bytes: password length (default: 8 characters)
48 - storage-path: plain-text persistent storage for password,
49 that can only be accessed by the user
50 (default: ${buildout:parts-directory}/${:_buildout_section_name_})
51 """
52
53 def __init__(self, buildout, name, options):
54 options_get = options.get
55 try:
56 self.storage_path = options['storage-path']
57 except KeyError:
58 self.storage_path = options['storage-path'] = os.path.join(
59 buildout['buildout']['parts-directory'], name)
60 try:
61 with open(self.storage_path) as f:
62 passwd = f.read()
63 except IOError as e:
64 if e.errno != errno.ENOENT:
65 raise
66 passwd = None
67 if not passwd:
68 passwd = self.generatePassword(int(options_get('bytes', '8')))
69 self.update = self.install
70 self.passwd = passwd
71 # Password must not go into .installed file, for 2 reasons:
72 # security of course but also to prevent buildout to always reinstall.
73 options.get = lambda option, *args, **kw: passwd \
74 if option == 'passwd' else options_get(option, *args, **kw)
75
76 generatePassword = staticmethod(generatePassword)
77
78 def install(self):
79 if self.storage_path:
80 try:
81 os.unlink(self.storage_path)
82 except OSError as e:
83 if e.errno != errno.ENOENT:
84 raise
85 fd = os.open(self.storage_path,
86 os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_TRUNC, 0600)
87 try:
88 os.write(fd, self.passwd)
89 finally:
90 os.close(fd)
91 return self.storage_path
92
93 def update(self):
94 return ()