forked from privacyidea/privacyidea
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpi-manage.py
executable file
·554 lines (496 loc) · 18.1 KB
/
pi-manage.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 2015-06-16 Cornelius Kölbel <cornelius@privacyidea.org>
# Add creation of JWT token
# 2015-03-27 Cornelius Kölbel, cornelius@privacyidea.org
# Add sub command for policies
# 2014-12-15 Cornelius Kölbel, info@privacyidea.org
# Initial creation
#
# (c) Cornelius Kölbel
# Info: http://www.privacyidea.org
#
# This code is free software; you can redistribute it and/or
# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
# License as published by the Free Software Foundation; either
# version 3 of the License, or any later version.
#
# This code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# ./manage.py db init
# ./manage.py db migrate
# ./manage.py createdb
#
import os
import sys
import datetime
from datetime import timedelta
import re
from subprocess import call, Popen
from getpass import getpass
from privacyidea.lib.security.default import DefaultSecurityModule
from privacyidea.lib.crypto import geturandom
from privacyidea.lib.auth import (create_db_admin, list_db_admin,
delete_db_admin)
from privacyidea.lib.policy import (delete_policy, enable_policy,
PolicyClass, set_policy)
from privacyidea.app import create_app
from privacyidea.lib.auth import ROLE
from flask.ext.script import Manager
from privacyidea.app import db
from flask.ext.migrate import MigrateCommand
# Wee need to import something, so that the models will be created.
from privacyidea.models import Admin
from sqlalchemy import create_engine, desc, MetaData
from sqlalchemy.orm import sessionmaker
from privacyidea.lib.auditmodules.sqlaudit import LogEntry
from Crypto.PublicKey import RSA
import jwt
app = create_app(config_name='production')
manager = Manager(app)
admin_manager = Manager(usage='Create new administrators or modify existing '
'ones.')
backup_manager = Manager(usage='Create database backup and restore')
realm_manager = Manager(usage='Create new realms')
resolver_manager = Manager(usage='Create new resolver')
policy_manager = Manager(usage='Manage policies')
api_manager = Manager(usage="Manage API keys")
manager.add_command('db', MigrateCommand)
manager.add_command('admin', admin_manager)
manager.add_command('backup', backup_manager)
manager.add_command('realm', realm_manager)
manager.add_command('resolver', resolver_manager)
manager.add_command('policy', policy_manager)
manager.add_command('api', api_manager)
@admin_manager.command
def add(username, email, password=None):
"""
Register a new administrator in the database.
"""
db.create_all()
if not password:
password = getpass()
password2 = getpass(prompt='Confirm: ')
if password != password2:
import sys
sys.exit('Error: passwords do not match.')
create_db_admin(app, username, email, password)
print('Admin {0} was registered successfully.'.format(username))
@admin_manager.command
def list():
"""
List all administrators.
"""
list_db_admin()
@admin_manager.command
def delete(username):
"""
Delete an existing administrator.
"""
delete_db_admin(username)
@admin_manager.command
def change(username, email=None, password_prompt=False):
"""
Change the email address or the password of an existing administrator.
"""
if password_prompt:
password = getpass()
password2 = getpass(prompt='Confirm: ')
if password != password2:
import sys
sys.exit('Error: passwords do not match.')
else:
password = None
create_db_admin(app, username, email, password)
@backup_manager.command
def create(directory="/var/lib/privacyidea/backup/"):
"""
Create a new backup of the database and the configuration. This does not
backup the encryption key!
"""
CONF_DIR = "/etc/privacyidea/"
# INIFILE = "%s/pi.cfg" % CONF_DIR
DATE = datetime.datetime.now().strftime("%Y%m%d-%H%M")
BASE_NAME = "privacyidea-backup"
sqlfile = "%s/dbdump-%s.sql" % (directory, DATE)
backup_file = "%s/%s-%s.tgz" % (directory, BASE_NAME, DATE)
sqluri = app.config.get("SQLALCHEMY_DATABASE_URI")
sqltype = sqluri.split(":")[0]
if sqltype == "sqlite":
productive_file = sqluri[len("sqlite:///"):]
print "Backup SQLite %s" % productive_file
sqlfile = "%s/db-%s.sqlite" % (directory, DATE)
call(["cp", productive_file, sqlfile])
elif sqltype == "mysql":
m = re.match("mysql://(.*):(.*)@(.*)/(.*)", sqluri)
username = m.groups()[0]
password = m.groups()[1]
datahost = m.groups()[2]
database = m.groups()[3]
call("mysqldump -u %s --password=%s -h %s %s > %s" % (username,
password,
datahost,
database,
sqlfile),
shell=True)
else:
print "unsupported SQL syntax: %s" % sqltype
sys.exit(2)
call(["mkdir", "-p", directory])
call(["tar", "-zcf", backup_file, CONF_DIR, sqlfile])
os.unlink(sqlfile)
os.chmod(backup_file, 0600)
@backup_manager.command
def restore(backup_file):
"""
Restore a previously made backup. You need to specify the tgz file.
"""
SQLALCHEMY_DATABASE_URI = None
directory = os.path.dirname(backup_file)
call(["tar", "-zxf", backup_file, "-C", "/"])
print 60*"="
"""
The restore of the SQL file will not work, since at the moment we "
can not be sure to know the correct SQLALCHEMY_DATABASE_URI. The "
right URI "
was just restored to /etc/privacyidea/pi.cfg. So please take a "
look into that file and restore the SQL dumb from the file "
/var/lib/privacyidea/backup/*.[sql,sqlite]")
"""
execfile("/etc/privacyidea/pi.cfg")
# Now we know the variable SQLALCHEMY_DATABASE_URI
sqluri = SQLALCHEMY_DATABASE_URI
if sqluri is None:
print "No SQLALCHEMY_DATABASE_URI found in /etc/privacyidea/pi.cfg"
sys.exit(2)
sqltype = sqluri.split(":")[0]
if sqltype == "sqlite":
productive_file = sqluri[len("sqlite:///"):]
print "Restore SQLite %s" % productive_file
sqlfile = "%s/db-*.sqlite" % (directory)
call(["cp", sqlfile, productive_file])
os.unlink(sqlfile)
elif sqltype == "mysql":
m = re.match("mysql://(.*):(.*)@(.*)/(.*)", sqluri)
username = m.groups()[0]
password = m.groups()[1]
datahost = m.groups()[2]
database = m.groups()[3]
sqlfile = "%s/dbdump-*.sql" % (directory)
call("mysql -u %s --password=%s -h %s %s < %s" % (username,
password,
datahost,
database,
sqlfile), shell=True)
os.unlink(sqlfile)
else:
print "unsupported SQL syntax: %s" % sqltype
sys.exit(2)
@manager.command
def test():
"""
Run all nosetests.
"""
call(['nosetests', '-v',
'--with-coverage', '--cover-package=privacyidea', '--cover-branches',
'--cover-erase', '--cover-html', '--cover-html-dir=cover'])
@manager.command
def encrypt_enckey(encfile):
"""
You will be asked for a password and the encryption key in the specified
file will be encrypted with an AES key derived from your password.
The encryption key in the file is a 96 bit binary key.
The password based encrypted encryption key is a hex combination of an IV
and the encrypted data.
The result can be piped to a new enckey file.
"""
password = getpass()
password2 = getpass(prompt='Confirm: ')
if password != password2:
import sys
sys.exit('Error: passwords do not match.')
f = open(encfile)
enckey = f.read()
f.close()
res = DefaultSecurityModule.password_encrypt(enckey, password)
print res
@manager.command
def create_enckey():
"""
If the key of the given configuration does not exist, it will be created
"""
print
filename = app.config.get("PI_ENCFILE")
if os.path.isfile(filename):
print("The file \n\t%s\nalready exist. We do not overwrite it!" %
filename)
sys.exit(1)
f = open(filename, "w")
f.write(DefaultSecurityModule.random(96))
f.close()
print "Encryption key written to %s" % filename
print "Please ensure to set the access rights for the correct user to 400!"
@manager.command
def create_audit_keys(keysize=2048):
"""
Create the RSA signing keys for the audit log.
You may specify an additional keysize.
The default keysize is 2048 bit.
"""
filename = app.config.get("PI_AUDIT_KEY_PRIVATE")
if os.path.isfile(filename):
print("The file \n\t%s\nalready exist. We do not overwrite it!" %
filename)
sys.exit(1)
new_key = RSA.generate(keysize, e=65537)
public_key = new_key.publickey().exportKey("PEM")
private_key = new_key.exportKey("PEM")
f = open(filename, "w")
f.write(private_key)
f.close()
f = open(app.config.get("PI_AUDIT_KEY_PUBLIC"), "w")
f.write(public_key)
f.close()
print("Signing keys written to %s and %s" %
(filename, app.config.get("PI_AUDIT_KEY_PUBLIC")))
print("Please ensure to set the access rights for the correct user to 400!")
@manager.command
def createdb():
"""
Initially create the tables in the database. The database must exist.
(SQLite database will be created)
"""
print db
db.create_all()
db.session.commit()
@manager.option('--highwatermark', help="If entries exceed this value, "
"old entries are deleted.")
@manager.option('--lowwatermark', help="Keep this number of entries.")
def rotate_audit(highwatermark=10000, lowwatermark=5000):
"""
Rotate the SQL audit log.
If more than 'highwatermark' entries are in the audit log old entries
will be deleted, so that 'lowwatermark' entries remain.
"""
metadata = MetaData()
highwatermark = int(highwatermark or 10000)
lowwatermark = int(lowwatermark or 5000)
default_module = "privacyidea.lib.auditmodules.sqlaudit"
token_db_uri = app.config.get("SQLALCHEMY_DATABASE_URI")
audit_db_uri = app.config.get("PI_AUDIT_SQL_URI", token_db_uri)
audit_module = app.config.get("PI_AUDIT_MODULE", default_module)
if audit_module != default_module:
raise Exception("We only rotate SQL audit module. You are using %s" %
audit_module)
print("Cleaning up with high: %s, low: %s. %s" % (highwatermark,
lowwatermark,
audit_db_uri))
engine = create_engine(audit_db_uri)
# create a configured "Session" class
session = sessionmaker(bind=engine)()
# create a Session
metadata.create_all(engine)
count = session.query(LogEntry.id).count()
for l in session.query(LogEntry.id).order_by(desc(LogEntry.id)).limit(1):
last_id = l[0]
print("The log audit log has %i entries, the last one is %i" % (count,
last_id))
# deleting old entries
if count > highwatermark:
print("More than %i entries, deleting..." % highwatermark)
cut_id = last_id - lowwatermark
# delete all entries less than cut_id
print("Deleting entries smaller than %i" % cut_id)
session.query(LogEntry.id).filter(LogEntry.id < cut_id).delete()
session.commit()
@resolver_manager.command
def create(name, rtype, filename):
"""
Create a new resolver with name and type (ldapresolver, sqlresolver).
Read the necessary resolver parameters from the filename. The file should
contain a python dictionary.
:param name: The name of the resolver
:param rtype: The type of the resolver like ldapresolver or sqlresolver
:param filename: The name of the config file.
:return:
"""
from privacyidea.lib.resolver import save_resolver
import ast
f = open(filename, 'r')
contents = f.read()
f.close()
params = ast.literal_eval(contents)
params["resolver"] = name
params["type"] = rtype
save_resolver(params)
@resolver_manager.command
def create_internal(name):
"""
This creates a new internal, editable sqlresolver. The users will be
stored in the token database in a table called 'users_<name>'. You can then
add this resolver to a new real using the command 'pi-manage.py realm'.
"""
from privacyidea.lib.resolver import save_resolver
sqluri = app.config.get("SQLALCHEMY_DATABASE_URI")
sqlelements = sqluri.split("/")
# mysql://user:password@localhost/pi
# sqlite:////home/cornelius/src/privacyidea/data.sqlite
sql_driver = sqlelements[0][:-1]
user_pw_host = sqlelements[2]
database = "/".join(sqlelements[3:])
username = ""
password = ""
# determine host and user
hostparts = user_pw_host.split("@")
if len(hostparts) > 2:
print "Invalid database URI: %s" % sqluri
sys.exit(2)
elif len(hostparts) == 1:
host = hostparts[0] or "/"
elif len(hostparts) == 2:
host = hostparts[1] or "/"
# split hostname and password
userparts = hostparts[0].split(":")
if len(userparts) == 2:
username = userparts[0]
password = userparts[1]
elif len(userparts) == 1:
username = userparts[0]
else:
print "Invalid username and password in database URI: %s" % sqluri
sys.exit(3)
# now we can create the resolver
params = {'resolver': name,
'type': "sqlresolver",
'Server': host,
'Driver': sql_driver,
'User': username,
'Password': password,
'Database': database,
'Table': 'users_' + name,
'Limit': '500',
'Editable': '1',
'Map': '{"userid": "id", "username": "username", '
'"email":"email", "password": "password", '
'"phone":"phone", "mobile":"mobile", "surname":"surname", '
'"givenname":"givenname", "description": "description"}'}
save_resolver(params)
# Now we create the database table
from sqlalchemy import create_engine
from sqlalchemy import Table, MetaData, Column
from sqlalchemy import Integer, String
engine = create_engine(sqluri)
metadata = MetaData()
Table('users_%s' % name,
metadata,
Column('id', Integer, primary_key=True),
Column('username', String(40), unique=True),
Column('email', String(80)),
Column('password', String(255)),
Column('phone', String(40)),
Column('mobile', String(40)),
Column('surname', String(40)),
Column('givenname', String(40)),
Column('description', String(255)))
metadata.create_all(engine)
@resolver_manager.command
def list():
"""
list the available resolvers and the type
"""
from privacyidea.lib.resolver import get_resolver_list
resolver_list = get_resolver_list()
for name, resolver in resolver_list.iteritems():
print "%16s - (%s)" % (name, resolver.get("type"))
@realm_manager.command
def list():
"""
list the available realms
"""
from privacyidea.lib.realm import get_realms
realm_list = get_realms()
for name, realm_data in realm_list.iteritems():
resolvernames = [x.get("name") for x in realm_data.get("resolver")]
print "%16s: %s" % (name, resolvernames)
@realm_manager.command
def create(name, resolver):
"""
Create a new realm.
This will create a new realm with the given resolver.
*restriction*: The new realm will only contain one resolver!
:return:
"""
from privacyidea.lib.realm import set_realm
set_realm(name, [resolver])
# Policy interface
@policy_manager.command
def list():
"""
list the policies
"""
P = PolicyClass()
policies = P.get_policies()
print "Active \t Name \t Scope"
print 40*"="
for policy in policies:
print("%s \t %s \t %s" % (policy.get("active"), policy.get("name"),
policy.get("scope")))
@policy_manager.command
def enable(name):
"""
enable a policy by name
"""
r = enable_policy(name)
print r
@policy_manager.command
def disable(name):
"""
disable a policy by name
"""
r = enable_policy(name, False)
print r
@policy_manager.command
def delete(name):
"""
delete a policy by name
"""
r = delete_policy(name)
print r
@policy_manager.command
def create(name, scope, action):
"""
create a new policy
"""
r = set_policy(name, scope, action)
return r
@api_manager.command
def createtoken(role=ROLE.ADMIN):
"""
Create an API authentication token
for administrative or validate use.
Possible roles are "admin" or "validate".
"""
username = geturandom(hex=True)
secret = app.config.get("SECRET_KEY")
authtype = "API"
validity = timedelta(days=365)
token = jwt.encode({"username": username,
"realm": "API",
"nonce": geturandom(hex=True),
"role": role,
"authtype": authtype,
"exp": datetime.datetime.utcnow() + validity,
"rights": "TODO"},
secret)
print "Username: %s" % username
print "Role: %s" % role
print "Auth-Token: %s" % token
if __name__ == '__main__':
manager.run()