From d417cfb8dc2584940420e28dffce196addfc6d17 Mon Sep 17 00:00:00 2001 From: Ivan Filippov Date: Mon, 11 Apr 2016 06:11:02 -0600 Subject: [PATCH 01/70] Initial support for LDAP group based security. --- README.md | 8 ++++++++ app/models.py | 39 +++++++++++++++++++++++++++++++++++---- config_template.py | 3 +++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aeeb307..086b011 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,14 @@ Web application configuration is stored in `config.py` file. Let's clone it from (flask)$ vim config.py ``` +You can configure group based security by tweaking the below parameters in `config.py`. Groups membership comes from LDAP. +Setting `LDAP_GROUP_SECURITY` to True enables group-based security. With this enabled only members of the two groups listed below are allowed to login. Members of `LDAP_ADMIN_GROUP` will get the Administrator role and members of `LDAP_USER_GROUP` will get the User role. Sample config below: +``` +LDAP_GROUP_SECURITY = True +LDAP_ADMIN_GROUP = 'CN=PowerDNS-Admin Admin,OU=Custom,DC=ivan,DC=local' +LDAP_USER_GROUP = 'CN=PowerDNS-Admin User,OU=Custom,DC=ivan,DC=local' +``` + Create database after having proper configs ``` (flask)% ./createdb.py diff --git a/app/models.py b/app/models.py index 48ed39a..84093d4 100644 --- a/app/models.py +++ b/app/models.py @@ -19,6 +19,9 @@ LDAP_USERNAME = app.config['LDAP_USERNAME'] LDAP_PASSWORD = app.config['LDAP_PASSWORD'] LDAP_SEARCH_BASE = app.config['LDAP_SEARCH_BASE'] LDAP_TYPE = app.config['LDAP_TYPE'] +LDAP_GROUP_SECURITY = app.config['LDAP_GROUP_SECURITY'] +LDAP_ADMIN_GROUP = app.config['LDAP_ADMIN_GROUP'] +LDAP_USER_GROUP = app.config['LDAP_USER_GROUP'] PDNS_STATS_URL = app.config['PDNS_STATS_URL'] PDNS_API_KEY = app.config['PDNS_API_KEY'] @@ -172,6 +175,25 @@ class User(db.Model): try: ldap_username = result[0][0][0] l.simple_bind_s(ldap_username, self.password) + if LDAP_GROUP_SECURITY: + try: + groupSearchFilter = "(&(objectcategory=group)(member=%s))" % ldap_username + groups = self.ldap_search(groupSearchFilter, LDAP_SEARCH_BASE) + allowedlogin = False + isadmin = False + for group in groups: + if (group[0][0] == LDAP_ADMIN_GROUP): + allowedlogin = True + isadmin = True + logging.info('User %s is part of the "%s" group that allows admin access to PowerDNS-Admin' % (self.username,LDAP_ADMIN_GROUP)) + if (group[0][0] == LDAP_USER_GROUP): + allowedlogin = True + logging.info('User %s is part of the "%s" group that allows user access to PowerDNS-Admin' % (self.username,LDAP_USER_GROUP)) + if allowedlogin == False: + logging.error('User %s is not part of the "%s" or "%s" groups that allow access to PowerDNS-Admin' % (self.username,LDAP_ADMIN_GROUP,LDAP_USER_GROUP)) + return False + except: + logging.error('LDAP group lookup for user %s has failed' % self.username) logging.info('User "%s" logged in successfully' % self.username) # create user if not exist in the db @@ -185,17 +207,26 @@ class User(db.Model): self.firstname = self.username self.lastname = '' - # first register user will be in Administrator role - if User.query.count() == 0: + # first registered user will be in Administrator role or if part of LDAP Admin group + if (User.query.count() == 0): self.role_id = Role.query.filter_by(name='Administrator').first().id else: - self.role_id = Role.query.filter_by(name='User').first().id + self.role_id = Role.query.filter_by(name='User').first().id + + # + if LDAP_GROUP_SECURITY: + if isadmin == True: + self.role_id = Role.query.filter_by(name='Administrator').first().id self.create_user() logging.info('Created user "%s" in the DB' % self.username) + else: + # user already exists in database, set their admin status based on group membership (if enabled) + if LDAP_GROUP_SECURITY: + self.set_admin(isadmin) return True except: - logging.error('User "%s" input a wrong password' % self.username) + logging.error('User "%s" input a wrong password(stage2)' % self.username) return False else: logging.error('Unsupported authentication method') diff --git a/config_template.py b/config_template.py index 12a3ca7..e5a0484 100644 --- a/config_template.py +++ b/config_template.py @@ -27,6 +27,9 @@ LDAP_USERNAME = 'cn=dnsuser,ou=users,ou=services,dc=duykhanh,dc=me' LDAP_PASSWORD = 'dnsuser' LDAP_SEARCH_BASE = 'ou=System Admins,ou=People,dc=duykhanh,dc=me' LDAP_TYPE = 'ldap' // or 'ad' +LDAP_GROUP_SECURITY = False // or True +LDAP_ADMIN_GROUP = 'CN=PowerDNS-Admin Admin,OU=Custom,DC=ivan,DC=local' +LDAP_USER_GROUP = 'CN=PowerDNS-Admin User,OU=Custom,DC=ivan,DC=local' # POWERDNS CONFIG PDNS_STATS_URL = 'http://172.16.214.131:8081/' From 05944e8585c846609c1e70bbfa2a47622c55aedc Mon Sep 17 00:00:00 2001 From: Ivan Filippov Date: Mon, 11 Apr 2016 10:22:40 -0600 Subject: [PATCH 02/70] Don't require LDAP group parameters if LDAP_GROUP_SECURITY is not chosen --- app/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models.py b/app/models.py index 84093d4..811a151 100644 --- a/app/models.py +++ b/app/models.py @@ -20,8 +20,9 @@ LDAP_PASSWORD = app.config['LDAP_PASSWORD'] LDAP_SEARCH_BASE = app.config['LDAP_SEARCH_BASE'] LDAP_TYPE = app.config['LDAP_TYPE'] LDAP_GROUP_SECURITY = app.config['LDAP_GROUP_SECURITY'] -LDAP_ADMIN_GROUP = app.config['LDAP_ADMIN_GROUP'] -LDAP_USER_GROUP = app.config['LDAP_USER_GROUP'] +if LDAP_GROUP_SECURITY == True: + LDAP_ADMIN_GROUP = app.config['LDAP_ADMIN_GROUP'] + LDAP_USER_GROUP = app.config['LDAP_USER_GROUP'] PDNS_STATS_URL = app.config['PDNS_STATS_URL'] PDNS_API_KEY = app.config['PDNS_API_KEY'] From 5914c3cc867e1be9365e3eeff0dbfb64195f84e2 Mon Sep 17 00:00:00 2001 From: Ivan Filippov Date: Tue, 12 Apr 2016 21:12:51 -0600 Subject: [PATCH 03/70] Add group-based security implementation for non-AD LDAP servers. --- app/models.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/models.py b/app/models.py index 811a151..2af7e6a 100644 --- a/app/models.py +++ b/app/models.py @@ -178,11 +178,16 @@ class User(db.Model): l.simple_bind_s(ldap_username, self.password) if LDAP_GROUP_SECURITY: try: - groupSearchFilter = "(&(objectcategory=group)(member=%s))" % ldap_username + if LDAP_TYPE == 'ldap': + uid = result[0][0][1]['uid'][0] + groupSearchFilter = "(&(objectClass=posixGroup)(memberUid=%s))" % uid + else: + groupSearchFilter = "(&(objectcategory=group)(member=%s))" % ldap_username groups = self.ldap_search(groupSearchFilter, LDAP_SEARCH_BASE) allowedlogin = False isadmin = False for group in groups: + logging.debug(group) if (group[0][0] == LDAP_ADMIN_GROUP): allowedlogin = True isadmin = True @@ -194,7 +199,7 @@ class User(db.Model): logging.error('User %s is not part of the "%s" or "%s" groups that allow access to PowerDNS-Admin' % (self.username,LDAP_ADMIN_GROUP,LDAP_USER_GROUP)) return False except: - logging.error('LDAP group lookup for user %s has failed' % self.username) + logging.error('LDAP group lookup for user "%s" has failed' % self.username) logging.info('User "%s" logged in successfully' % self.username) # create user if not exist in the db @@ -227,7 +232,7 @@ class User(db.Model): self.set_admin(isadmin) return True except: - logging.error('User "%s" input a wrong password(stage2)' % self.username) + logging.error('User "%s" input a wrong password' % self.username) return False else: logging.error('Unsupported authentication method') From 03e0f5079507c1177518c2032bd86663a0a992d9 Mon Sep 17 00:00:00 2001 From: ssendev Date: Thu, 18 Aug 2016 22:05:15 +0200 Subject: [PATCH 04/70] Allow to change root domain record via dyndns --- app/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views.py b/app/views.py index a33e6e1..756c413 100644 --- a/app/views.py +++ b/app/views.py @@ -762,12 +762,12 @@ def dyndns_update(): domain = None domain_segments = hostname.split('.') for index in range(len(domain_segments)): - domain_segments.pop(0) full_domain = '.'.join(domain_segments) potential_domain = Domain.query.filter(Domain.name == full_domain).first() if potential_domain in domains: domain = potential_domain break + domain_segments.pop(0) if not domain: history = History(msg="DynDNS update: attempted update of %s but it does not exist for this user" % hostname, created_by=current_user.username) From a9408a4bd928a7f63e080c9a137a4871dcb3be7d Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Tue, 31 Oct 2017 16:18:48 +0100 Subject: [PATCH 05/70] updated requirement to support saml --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 46ab6ba..f8c609b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ pyotp==2.2.1 qrcode==5.3 Flask-OAuthlib==0.9.3 dnspython>=1.12.0 +python-saml==2.3.0 \ No newline at end of file From f067d0d5f07732c61bc37336daae3ff5a8c34a7d Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Tue, 31 Oct 2017 18:14:38 +0100 Subject: [PATCH 06/70] fixed requirements. caused redirect loop --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index f8c609b..293621d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -Flask>=0.10 -Flask-WTF>=0.11 -Flask-Login>=0.2.11 +Flask==0.12.2 +Flask-WTF==0.14.2 +Flask-Login==0.4.0 configobj==5.0.5 bcrypt==3.1.0 requests==2.7.0 From 9cc37000b5239ad9993097db7b53dbd279eac6c6 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Tue, 31 Oct 2017 19:20:07 +0100 Subject: [PATCH 07/70] updated gitignore to support saml --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d548024..d85a40a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ nosetests.xml flask config.py logfile.log +settings.json +advanced_settings.json db_repository/* upload/avatar/* From 4a661823e845baa8b011690406bca34adf852666 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Tue, 31 Oct 2017 19:20:53 +0100 Subject: [PATCH 08/70] added saml templates --- saml/template_advanced_settings.json | 29 +++++++++++++++++++++++++++ saml/template_settings.json | 30 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 saml/template_advanced_settings.json create mode 100644 saml/template_settings.json diff --git a/saml/template_advanced_settings.json b/saml/template_advanced_settings.json new file mode 100644 index 0000000..e336fe9 --- /dev/null +++ b/saml/template_advanced_settings.json @@ -0,0 +1,29 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": false, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": false, + "wantNameIdEncrypted": false + }, + "contactPerson": { + "technical": { + "givenName": "technical_name", + "emailAddress": "technical@example.com" + }, + "support": { + "givenName": "support_name", + "emailAddress": "support@example.com" + } + }, + "organization": { + "en-US": { + "name": "sp_test", + "displayname": "SP test", + "url": "http://sp.example.com" + } + } +} \ No newline at end of file diff --git a/saml/template_settings.json b/saml/template_settings.json new file mode 100644 index 0000000..deb7cc1 --- /dev/null +++ b/saml/template_settings.json @@ -0,0 +1,30 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "http://127.0.0.1/saml/metadata", + "assertionConsumerService": { + "url": "http://127.0.0.1/saml/authorized", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "singleLogoutService": { + "url": "https://127.0.0.1/saml/sls", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", + "x509cert": "", + "privateKey": "" + }, + "idp": { + "entityId": "https://app.onelogin.com/saml/metadata", + "singleSignOnService": { + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "singleLogoutService": { + "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "" + } +} \ No newline at end of file From 933d678e83495a7d19553ade7d881e122fe7397a Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Tue, 31 Oct 2017 19:21:22 +0100 Subject: [PATCH 09/70] added SAML auth basics and metadata --- app/lib/utils.py | 17 +++++++++++++++++ app/templates/login.html | 7 ++++++- app/views.py | 25 +++++++++++++++++++++++++ config_template.py | 3 +++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/app/lib/utils.py b/app/lib/utils.py index a4a2954..1156da3 100644 --- a/app/lib/utils.py +++ b/app/lib/utils.py @@ -8,6 +8,9 @@ import hashlib from app import app from distutils.version import StrictVersion +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.utils import OneLogin_Saml2_Utils + if 'TIMEOUT' in app.config.keys(): TIMEOUT = app.config['TIMEOUT'] else: @@ -159,3 +162,17 @@ def email_to_gravatar_url(email, size=100): hash_string = hashlib.md5(email).hexdigest() return "https://s.gravatar.com/avatar/%s?s=%s" % (hash_string, size) + +def prepare_flask_request(request): + url_data = urlparse.urlparse(request.url) + return { + 'http_host': request.host, + 'server_port': url_data.port, + 'script_name': request.path, + 'get_data': request.args.copy(), + 'post_data': request.form.copy() + } + +def init_saml_auth(req): + auth = OneLogin_Saml2_Auth(req, custom_base_path=app.config['SAML_PATH']) + return auth \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html index 9527f22..1d0403a 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -98,11 +98,16 @@ + {% if saml_enabled %} +
+ SAML login + {% endif %} {% if github_enabled %} +
Github oauth login {% endif %} -
{% if signup_enabled %} +
Create an account {% endif %} diff --git a/app/views.py b/app/views.py index 8cc8761..62ef5dd 100644 --- a/app/views.py +++ b/app/views.py @@ -20,6 +20,8 @@ from .models import User, Domain, Record, Server, History, Anonymous, Setting, D from app import app, login_manager, github from lib import utils +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.utils import OneLogin_Saml2_Utils jinja2.filters.FILTERS['display_record_name'] = utils.display_record_name jinja2.filters.FILTERS['display_master_name'] = utils.display_master_name @@ -166,6 +168,27 @@ def github_login(): return abort(400) return github.authorize(callback=url_for('authorized', _external=True)) +@app.route('/saml/login') +def saml_login(): + if not app.config.get('SAML_ENABLED'): + return abort(400) + return abort(400) + +@app.route('/saml/metadata/') +def saml_metadata(): + req = utils.prepare_flask_request(request) + auth = utils.init_saml_auth(req) + settings = auth.get_settings() + metadata = settings.get_sp_metadata() + errors = settings.validate_metadata(metadata) + + if len(errors) == 0: + resp = make_response(metadata, 200) + resp.headers['Content-Type'] = 'text/xml' + else: + resp = make_response(errors.join(', '), 500) + return resp + @app.route('/login', methods=['GET', 'POST']) @login_manager.unauthorized_handler def login(): @@ -175,6 +198,7 @@ def login(): BASIC_ENABLED = app.config['BASIC_ENABLED'] SIGNUP_ENABLED = app.config['SIGNUP_ENABLED'] GITHUB_ENABLE = app.config.get('GITHUB_OAUTH_ENABLE') + SAML_ENABLED = app.config.get('SAML_ENABLED') if g.user is not None and current_user.is_authenticated: return redirect(url_for('dashboard')) @@ -197,6 +221,7 @@ def login(): if request.method == 'GET': return render_template('login.html', github_enabled=GITHUB_ENABLE, + saml_enabled=SAML_ENABLED, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) diff --git a/config_template.py b/config_template.py index 288ff47..e80efd0 100644 --- a/config_template.py +++ b/config_template.py @@ -65,6 +65,9 @@ GITHUB_OAUTH_URL = 'http://127.0.0.1:5000/api/v3/' GITHUB_OAUTH_TOKEN = 'http://127.0.0.1:5000/oauth/token' GITHUB_OAUTH_AUTHORIZE = 'http://127.0.0.1:5000/oauth/authorize' +# SAML Authnetication +SAML_ENABLED = True + #Default Auth BASIC_ENABLED = True SIGNUP_ENABLED = True From 97d551e11dd05c56e819e5018776f37f010ee527 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Tue, 31 Oct 2017 19:27:15 +0100 Subject: [PATCH 10/70] ignore idp cert --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d85a40a..93d1207 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ config.py logfile.log settings.json advanced_settings.json +idp.crt db_repository/* upload/avatar/* From 805439e6ee287de6379bea42f02a60ce0344ba4f Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Tue, 31 Oct 2017 20:42:13 +0100 Subject: [PATCH 11/70] updated preapre_flask_request to support frontend-ssl --- app/lib/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/lib/utils.py b/app/lib/utils.py index 1156da3..f824ea0 100644 --- a/app/lib/utils.py +++ b/app/lib/utils.py @@ -164,13 +164,18 @@ def email_to_gravatar_url(email, size=100): return "https://s.gravatar.com/avatar/%s?s=%s" % (hash_string, size) def prepare_flask_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields url_data = urlparse.urlparse(request.url) return { + 'https': 'on' if request.scheme == 'https' else 'off', 'http_host': request.host, 'server_port': url_data.port, 'script_name': request.path, 'get_data': request.args.copy(), - 'post_data': request.form.copy() + 'post_data': request.form.copy(), + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + # 'lowercase_urlencoding': True, + 'query_string': request.query_string } def init_saml_auth(req): From 31eaee8e0ba4d10444967ec5be7dea96906a7da0 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Tue, 31 Oct 2017 22:38:26 +0100 Subject: [PATCH 12/70] added saml authentication --- app/views.py | 49 +++++++++++++++++++++++++++++++++++-- saml/advanced_settings.json | 29 ++++++++++++++++++++++ saml/settings.json | 30 +++++++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 saml/advanced_settings.json create mode 100644 saml/settings.json diff --git a/app/views.py b/app/views.py index 62ef5dd..17bbf5d 100644 --- a/app/views.py +++ b/app/views.py @@ -172,10 +172,15 @@ def github_login(): def saml_login(): if not app.config.get('SAML_ENABLED'): return abort(400) - return abort(400) + req = utils.prepare_flask_request(request) + auth = utils.init_saml_auth(req) + redirect_url=OneLogin_Saml2_Utils.get_self_url(req) + url_for('saml_authorized') + return redirect(auth.login(return_to=redirect_url)) -@app.route('/saml/metadata/') +@app.route('/saml/metadata') def saml_metadata(): + if not app.config.get('SAML_ENABLED'): + return abort(400) req = utils.prepare_flask_request(request) auth = utils.init_saml_auth(req) settings = auth.get_settings() @@ -189,6 +194,45 @@ def saml_metadata(): resp = make_response(errors.join(', '), 500) return resp +@app.route('/saml/authorized', methods=['GET', 'POST']) +def saml_authorized(): + errors = [] + if not app.config.get('SAML_ENABLED'): + return abort(400) + req = utils.prepare_flask_request(request) + auth = utils.init_saml_auth(req) + auth.process_response() + attributes = auth.get_attributes(); + not_auth_warn = not auth.is_authenticated() + if len(errors) == 0: + session['samlUserdata'] = auth.get_attributes() + session['samlNameId'] = auth.get_nameid() + session['samlSessionIndex'] = auth.get_session_index() + self_url = OneLogin_Saml2_Utils.get_self_url(req) + self_url = self_url+req['script_name'] + if 'RelayState' in request.form and self_url != request.form['RelayState']: + return redirect(auth.redirect_to(request.form['RelayState'])) + user = User.query.filter_by(username=session['samlNameId'].lower()).first() + if not user: + # create user + user = User(username=session['samlNameId'], + plain_text_password=gen_salt(7), + email=session['samlNameId']) + user.create_local_user() + session['user_id'] = user.id + if session['samlUserdata'].has_key("email"): + user.email = session['samlUserdata']["email"][0].lower() + if session['samlUserdata'].has_key("givenname"): + user.firstname = session['samlUserdata']["givenname"][0] + if session['samlUserdata'].has_key("surname"): + user.lastname = session['samlUserdata']["surname"][0] + user.plain_text_password = gen_salt(7) + user.update_profile() + login_user(user, remember=False) + return redirect(url_for('index')) + else: + return error(401,"an error occourred processing SAML response") + @app.route('/login', methods=['GET', 'POST']) @login_manager.unauthorized_handler def login(): @@ -288,6 +332,7 @@ def login(): def logout(): session.pop('user_id', None) session.pop('github_token', None) + session.clear() logout_user() return redirect(url_for('login')) diff --git a/saml/advanced_settings.json b/saml/advanced_settings.json new file mode 100644 index 0000000..5b36295 --- /dev/null +++ b/saml/advanced_settings.json @@ -0,0 +1,29 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": false, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": true, + "wantAssertionsSigned": true, + "wantNameIdEncrypted": false + }, + "contactPerson": { + "technical": { + "givenName": "ahd Service Operation Center", + "emailAddress": "servicedesk@ahd.de" + }, + "support": { + "givenName" : "ahd Service Operation Center", + "emailAddress": "servicedesk@ahd.de" + } + }, + "organization": { + "en-US": { + "name": "PowerDNS-Admin", + "displayname": "PowerDNS-Admin", + "url": "https://10.12.95.95" + } + } +} \ No newline at end of file diff --git a/saml/settings.json b/saml/settings.json new file mode 100644 index 0000000..2ca7872 --- /dev/null +++ b/saml/settings.json @@ -0,0 +1,30 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "http://10.12.95.95", + "assertionConsumerService": { + "url": "https://10.12.95.95/saml/authorized", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "singleLogoutService": { + "url": "https://10.12.95.95/saml/sls", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "x509cert": "", + "privateKey": "" + }, + "idp": { + "entityId": "http://fs.ahd-vcloud.biz/adfs/services/trust", + "singleSignOnService": { + "url": "https://fs.ahd-vcloud.biz/adfs/ls/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "singleLogoutService": { + "url": "https://fs.ahd-vcloud.biz/adfs/ls/", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "-----BEGIN CERTIFICATE-----MIIC3jCCAcagAwIBAgIQPD7o11EBtLZDvWevCYeIGjANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDEyBBREZTIFNpZ25pbmcgLSBmcy5haGQtdmNsb3VkLmJpejAeFw0xNzAyMjQwOTI5MTBaFw0xODAyMjQwOTI5MTBaMCsxKTAnBgNVBAMTIEFERlMgU2lnbmluZyAtIGZzLmFoZC12Y2xvdWQuYml6MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs8c+Yde1AQpBSikjKMXRY3FmvG64YT8MgJGPJ0CuTr0jyvsARvK51v0FiMsQh48uQ+KtXWNfBTrFee1CkpHQHw1UVRWQVToZzhiTgVBWc3XXzjfxThUe5IGfSQa11+s+/qxlfQZi2V1JhKUpXfYehbQIEJ5n0kzRzGfmZwZ8/A4gSOJGvFOLx0QTQ6scRUvgsDKJbmD3YWDweZwUGZkKSjKDbyNNNQKhwmpwFT2BLNadlscrgxjzDUQIaLnMQabE+DlQqYkxhM4LPvWcwL23dBIRRxIZlJ4oE/ZohtWtaHJewUTtWT3yfeDRD4d4Gxr5cgczwDhhlJtcrcmEmpHzkwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBU0ADCWI+W1uwXUPL7OXw90VHHQMxuMdClIK2Bwc0De0eaFFHvKWCk3mkdNf+SwxtPHnfAWDO7daxnY6HrqQbcO66gcMgDvgFkC3o5Ml9LBsFv/NmNCeB7+9xxkiYiCe68oitN5iR50JcwhZekpM9MtH8t36p6AWmnhfBt8LMxreuWobDRefx4aIIst8SPP13p4AOk5gTz07YbdMLYsUiTImBbLCcbqdFNMYPiZmUo7jEUnax05oh9vruFj3SltsR21S78ifUN/AmlpYvm+q3mW1q6ikltp6/HoVNMOCsEJqq7VL5jtdOmj2YpFf/twZF5pnbSqe3AZClBp4BufsKp-----END CERTIFICATE-----" + } +} \ No newline at end of file From dff5d7cf78cb4e854bcf654a016f92dc9ceb7375 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Tue, 31 Oct 2017 23:30:52 +0100 Subject: [PATCH 13/70] updated SAML metadata examples --- saml/advanced_settings.json | 29 --------------------------- saml/settings.json | 30 ---------------------------- saml/template_advanced_settings.json | 18 ++++++++--------- saml/template_settings.json | 16 +++++++-------- 4 files changed, 17 insertions(+), 76 deletions(-) delete mode 100644 saml/advanced_settings.json delete mode 100644 saml/settings.json diff --git a/saml/advanced_settings.json b/saml/advanced_settings.json deleted file mode 100644 index 5b36295..0000000 --- a/saml/advanced_settings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "security": { - "nameIdEncrypted": false, - "authnRequestsSigned": false, - "logoutRequestSigned": false, - "logoutResponseSigned": false, - "signMetadata": false, - "wantMessagesSigned": true, - "wantAssertionsSigned": true, - "wantNameIdEncrypted": false - }, - "contactPerson": { - "technical": { - "givenName": "ahd Service Operation Center", - "emailAddress": "servicedesk@ahd.de" - }, - "support": { - "givenName" : "ahd Service Operation Center", - "emailAddress": "servicedesk@ahd.de" - } - }, - "organization": { - "en-US": { - "name": "PowerDNS-Admin", - "displayname": "PowerDNS-Admin", - "url": "https://10.12.95.95" - } - } -} \ No newline at end of file diff --git a/saml/settings.json b/saml/settings.json deleted file mode 100644 index 2ca7872..0000000 --- a/saml/settings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "strict": true, - "debug": true, - "sp": { - "entityId": "http://10.12.95.95", - "assertionConsumerService": { - "url": "https://10.12.95.95/saml/authorized", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" - }, - "singleLogoutService": { - "url": "https://10.12.95.95/saml/sls", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - }, - "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - "x509cert": "", - "privateKey": "" - }, - "idp": { - "entityId": "http://fs.ahd-vcloud.biz/adfs/services/trust", - "singleSignOnService": { - "url": "https://fs.ahd-vcloud.biz/adfs/ls/", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - }, - "singleLogoutService": { - "url": "https://fs.ahd-vcloud.biz/adfs/ls/", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - }, - "x509cert": "-----BEGIN CERTIFICATE-----MIIC3jCCAcagAwIBAgIQPD7o11EBtLZDvWevCYeIGjANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDEyBBREZTIFNpZ25pbmcgLSBmcy5haGQtdmNsb3VkLmJpejAeFw0xNzAyMjQwOTI5MTBaFw0xODAyMjQwOTI5MTBaMCsxKTAnBgNVBAMTIEFERlMgU2lnbmluZyAtIGZzLmFoZC12Y2xvdWQuYml6MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs8c+Yde1AQpBSikjKMXRY3FmvG64YT8MgJGPJ0CuTr0jyvsARvK51v0FiMsQh48uQ+KtXWNfBTrFee1CkpHQHw1UVRWQVToZzhiTgVBWc3XXzjfxThUe5IGfSQa11+s+/qxlfQZi2V1JhKUpXfYehbQIEJ5n0kzRzGfmZwZ8/A4gSOJGvFOLx0QTQ6scRUvgsDKJbmD3YWDweZwUGZkKSjKDbyNNNQKhwmpwFT2BLNadlscrgxjzDUQIaLnMQabE+DlQqYkxhM4LPvWcwL23dBIRRxIZlJ4oE/ZohtWtaHJewUTtWT3yfeDRD4d4Gxr5cgczwDhhlJtcrcmEmpHzkwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBU0ADCWI+W1uwXUPL7OXw90VHHQMxuMdClIK2Bwc0De0eaFFHvKWCk3mkdNf+SwxtPHnfAWDO7daxnY6HrqQbcO66gcMgDvgFkC3o5Ml9LBsFv/NmNCeB7+9xxkiYiCe68oitN5iR50JcwhZekpM9MtH8t36p6AWmnhfBt8LMxreuWobDRefx4aIIst8SPP13p4AOk5gTz07YbdMLYsUiTImBbLCcbqdFNMYPiZmUo7jEUnax05oh9vruFj3SltsR21S78ifUN/AmlpYvm+q3mW1q6ikltp6/HoVNMOCsEJqq7VL5jtdOmj2YpFf/twZF5pnbSqe3AZClBp4BufsKp-----END CERTIFICATE-----" - } -} \ No newline at end of file diff --git a/saml/template_advanced_settings.json b/saml/template_advanced_settings.json index e336fe9..6bd7fb7 100644 --- a/saml/template_advanced_settings.json +++ b/saml/template_advanced_settings.json @@ -5,25 +5,25 @@ "logoutRequestSigned": false, "logoutResponseSigned": false, "signMetadata": false, - "wantMessagesSigned": false, - "wantAssertionsSigned": false, + "wantMessagesSigned": true, + "wantAssertionsSigned": true, "wantNameIdEncrypted": false }, "contactPerson": { "technical": { - "givenName": "technical_name", - "emailAddress": "technical@example.com" + "givenName": "", + "emailAddress": "" }, "support": { - "givenName": "support_name", - "emailAddress": "support@example.com" + "givenName" : "", + "emailAddress": "" } }, "organization": { "en-US": { - "name": "sp_test", - "displayname": "SP test", - "url": "http://sp.example.com" + "name": "PowerDNS-Admin", + "displayname": "PowerDNS-Admin", + "url": "https://" } } } \ No newline at end of file diff --git a/saml/template_settings.json b/saml/template_settings.json index deb7cc1..13aa758 100644 --- a/saml/template_settings.json +++ b/saml/template_settings.json @@ -2,29 +2,29 @@ "strict": true, "debug": true, "sp": { - "entityId": "http://127.0.0.1/saml/metadata", + "entityId": "http://", "assertionConsumerService": { - "url": "http://127.0.0.1/saml/authorized", + "url": "https:///saml/authorized", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, "singleLogoutService": { - "url": "https://127.0.0.1/saml/sls", + "url": "https:///saml/sls", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "x509cert": "", "privateKey": "" }, "idp": { - "entityId": "https://app.onelogin.com/saml/metadata", + "entityId": "http:///adfs/services/trust", "singleSignOnService": { - "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + "url": "https:///adfs/ls/", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "singleLogoutService": { - "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + "url": "https:///adfs/ls/", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, - "x509cert": "" + "x509cert": "" } } \ No newline at end of file From f3093fe794a6351def9d4ade3c9f0971d68f8f75 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Tue, 31 Oct 2017 23:45:24 +0100 Subject: [PATCH 14/70] updated documentation and config-template --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ config_template.py | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c09aa7b..2957ed3 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ PowerDNS Web-GUI - Built by Flask - Multiple domain management - Local / LDAP user authentication - Support Two-factor authentication (TOTP) +- Support SAML authentication - User management - User access management based on domain - User activity logging @@ -84,6 +85,47 @@ Run the application and enjoy! (flask)$ ./run.py ``` +### SAML Authentication +SAML authentication is supported. In order to use it you have to create your own settings.json and advanced_settings.json based on the templates. +Following Assertions are supported and used by this application: +- nameidentifier in form of email address as user login +- email used as user email address +- givenname used as firstname +- surname used as lastname + +### ADFS claim rules as example +Microsoft Active Directory Federation Services can be used as Identity Provider for SAML login. +The Following rules should be configured to send all attribute information to PowerDNS-Admin. +The nameidentifier should be something stable from the idp side. All other attributes are update when singing in. + +#### sending the nameidentifier +Name-Identifiers Type is "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" +``` +c:[Type == ""] + => issue(Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, Value = c.Value, ValueType = c.ValueType, Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"] = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"); +``` + +#### sending the firstname +Name-Identifiers Type is "givenname" +``` +c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"] + => issue(Type = "givenname", Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, Value = c.Value, ValueType = c.ValueType, Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"] = "urn:oasis:names:tc:SAML:1.1:nameid-format:transient"); +``` + +#### sending the lastname +Name-Identifiers Type is "surname" +``` +c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"] + => issue(Type = "surname", Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, Value = c.Value, ValueType = c.ValueType, Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"] = "urn:oasis:names:tc:SAML:1.1:nameid-format:transient"); +``` + +#### sending the email +Name-Identifiers Type is "email" +``` +c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] + => issue(Type = "email", Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, Value = c.Value, ValueType = c.ValueType, Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"] = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"); +``` + ### Screenshots ![login page](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki/images/readme_screenshots/fullscreen-login.png?raw=true) ![dashboard](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki/images/readme_screenshots/fullscreen-dashboard.png?raw=true) diff --git a/config_template.py b/config_template.py index e80efd0..d2437f6 100644 --- a/config_template.py +++ b/config_template.py @@ -66,7 +66,7 @@ GITHUB_OAUTH_TOKEN = 'http://127.0.0.1:5000/oauth/token' GITHUB_OAUTH_AUTHORIZE = 'http://127.0.0.1:5000/oauth/authorize' # SAML Authnetication -SAML_ENABLED = True +SAML_ENABLED = False #Default Auth BASIC_ENABLED = True From 12c957bf5f08fa5eac9b21bb4a1f4adc37de1167 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 1 Nov 2017 01:34:29 +0100 Subject: [PATCH 15/70] disabled profile usage when authenticated externally --- app/templates/user_profile.html | 18 +++++++++--------- app/views.py | 11 ++++++++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/templates/user_profile.html b/app/templates/user_profile.html index 19201dc..2dae7e0 100644 --- a/app/templates/user_profile.html +++ b/app/templates/user_profile.html @@ -19,7 +19,7 @@
-

Edit my profile

+

Edit my profile{% if external_account %} [Disabled - Authenticated externally]{% endif %}

@@ -40,17 +40,17 @@
+ placeholder="{{ current_user.firstname }}" {% if external_account %}disabled{% endif %}>
+ placeholder="{{ current_user.lastname }}" {% if external_account %}disabled{% endif %}>
+ placeholder="{{ current_user.email }}" {% if external_account %}disabled{% endif %}>
@@ -72,7 +72,7 @@
+ id="file" name="file" {% if external_account %}disabled{% endif %}>
@@ -95,15 +95,15 @@
+ id="newpassword" {% if external_account %}disabled{% endif %} />
+ id="rpassword" {% if external_account %}disabled{% endif %} />
-
@@ -112,7 +112,7 @@
- + {% if current_user.otp_secret %}
diff --git a/app/views.py b/app/views.py index 17bbf5d..ff035e4 100644 --- a/app/views.py +++ b/app/views.py @@ -228,6 +228,7 @@ def saml_authorized(): user.lastname = session['samlUserdata']["surname"][0] user.plain_text_password = gen_salt(7) user.update_profile() + session['external_auth'] = True login_user(user, remember=False) return redirect(url_for('index')) else: @@ -259,6 +260,7 @@ def login(): user.create_local_user() session['user_id'] = user.id + session['external_auth'] = True login_user(user, remember = False) return redirect(url_for('index')) @@ -741,8 +743,11 @@ def admin_settings_edit(setting): @app.route('/user/profile', methods=['GET', 'POST']) @login_required def user_profile(): - if request.method == 'GET': - return render_template('user_profile.html') + external_account = False + if session.has_key('external_auth'): + external_account = session['external_auth'] + if request.method == 'GET' or external_account: + return render_template('user_profile.html', external_account=external_account) if request.method == 'POST': # get new profile info firstname = request.form['firstname'] if 'firstname' in request.form else '' @@ -777,7 +782,7 @@ def user_profile(): user = User(username=current_user.username, plain_text_password=new_password, firstname=firstname, lastname=lastname, email=email, avatar=save_file_name, reload_info=False) user.update_profile() - return render_template('user_profile.html') + return render_template('user_profile.html', external_account=external_account) @app.route('/user/avatar/') From baa960aad63029a1b1e220cfb0f5780176cad4fb Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 1 Nov 2017 13:31:41 +0100 Subject: [PATCH 16/70] raised password length to 30 for external accounts. fixed error_checking for saml-authentication --- app/views.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/views.py b/app/views.py index ff035e4..bd370b6 100644 --- a/app/views.py +++ b/app/views.py @@ -202,8 +202,7 @@ def saml_authorized(): req = utils.prepare_flask_request(request) auth = utils.init_saml_auth(req) auth.process_response() - attributes = auth.get_attributes(); - not_auth_warn = not auth.is_authenticated() + errors = auth.get_errors() if len(errors) == 0: session['samlUserdata'] = auth.get_attributes() session['samlNameId'] = auth.get_nameid() @@ -216,7 +215,7 @@ def saml_authorized(): if not user: # create user user = User(username=session['samlNameId'], - plain_text_password=gen_salt(7), + plain_text_password=gen_salt(30), email=session['samlNameId']) user.create_local_user() session['user_id'] = user.id @@ -226,7 +225,7 @@ def saml_authorized(): user.firstname = session['samlUserdata']["givenname"][0] if session['samlUserdata'].has_key("surname"): user.lastname = session['samlUserdata']["surname"][0] - user.plain_text_password = gen_salt(7) + user.plain_text_password = gen_salt(30) user.update_profile() session['external_auth'] = True login_user(user, remember=False) @@ -255,7 +254,7 @@ def login(): if not user: # create user user = User(username=user_info['name'], - plain_text_password=gen_salt(7), + plain_text_password=gen_salt(30), email=user_info['email']) user.create_local_user() From f92661c7536e362932f018d920757c1e872a40ba Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 1 Nov 2017 13:40:26 +0100 Subject: [PATCH 17/70] remove unnecessary controls from profile for ext. auth. --- app/templates/user_profile.html | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/templates/user_profile.html b/app/templates/user_profile.html index 2dae7e0..28289ff 100644 --- a/app/templates/user_profile.html +++ b/app/templates/user_profile.html @@ -29,10 +29,10 @@ Info
  • Change Avatar
  • -
  • Change + {% if not external_account %}
  • Change Password
  • Authentication -
  • + {% endif %}>
    @@ -51,10 +51,10 @@ -
    +
    {% if not external_account %}
    -
    +
    {% endif %}
    @@ -69,25 +69,25 @@ else %} {% endif %} -
    +
    {% if not external_account %}
    -
    -
    + id="file" name="file"> +
    {% endif %} +
    {% if not external_account %}
    NOTE!  Only supports .PNG, .JPG, .JPEG. The best size to use is 200x200. -
    - + {% endif %} + {% if not external_account %}
    -
    + {% endif %} -
    + {% if not external_account %}
    {% if not current_user.password %} Your account password is managed via LDAP which isn't supported to change here. {% else %} @@ -124,7 +124,7 @@ {% endif %}
    -
    + {% endif %} From cd3b41553deb8847b1a30ff7d05ae75d2dbe2647 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 1 Nov 2017 13:55:57 +0100 Subject: [PATCH 18/70] fixed link for alternative login methods --- app/views.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/app/views.py b/app/views.py index bd370b6..5cc4c0b 100644 --- a/app/views.py +++ b/app/views.py @@ -293,19 +293,38 @@ def login(): try: auth = user.is_validate(method=auth_method) if auth == False: - return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) + return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED, + login_title=LOGIN_TITLE, + basic_enabled=BASIC_ENABLED, + signup_enabled=SIGNUP_ENABLED, + github_enabled=GITHUB_ENABLE, + saml_enabled=SAML_ENABLED) except Exception, e: error = e.message['desc'] if 'desc' in e.message else e - return render_template('login.html', error=error, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) + return render_template('login.html', error=error, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, + basic_enabled=BASIC_ENABLED, + signup_enabled=SIGNUP_ENABLED, + github_enabled=GITHUB_ENABLE, + saml_enabled=SAML_ENABLED) # check if user enabled OPT authentication if user.otp_secret: if otp_token: good_token = user.verify_totp(otp_token) if not good_token: - return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) + return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED, + login_title=LOGIN_TITLE, + basic_enabled=BASIC_ENABLED, + signup_enabled=SIGNUP_ENABLED, + github_enabled=GITHUB_ENABLE, + saml_enabled=SAML_ENABLED) else: - return render_template('login.html', error='Token required', ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) + return render_template('login.html', error='Token required', ldap_enabled=LDAP_ENABLED, + login_title=LOGIN_TITLE, + basic_enabled=BASIC_ENABLED, + signup_enabled=SIGNUP_ENABLED, + github_enabled = GITHUB_ENABLE, + saml_enabled = SAML_ENABLED) login_user(user, remember = remember_me) return redirect(request.args.get('next') or url_for('index')) @@ -322,7 +341,9 @@ def login(): try: result = user.create_local_user() if result == True: - return render_template('login.html', username=username, password=password, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) + return render_template('login.html', username=username, password=password, ldap_enabled=LDAP_ENABLED, + login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED, + github_enabled=GITHUB_ENABLE,saml_enabled=SAML_ENABLED) else: return render_template('register.html', error=result) except Exception, e: From 12cb6f28fb0b35b8b24dceee2101ff42438bbf41 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 1 Nov 2017 17:31:51 +0100 Subject: [PATCH 19/70] implemented dynamic metadata lookup removed saml json-templates --- app/lib/utils.py | 86 +++++++++++++++++++++++++++- config_template.py | 9 +++ saml/template_advanced_settings.json | 29 ---------- saml/template_settings.json | 30 ---------- 4 files changed, 93 insertions(+), 61 deletions(-) delete mode 100644 saml/template_advanced_settings.json delete mode 100644 saml/template_settings.json diff --git a/app/lib/utils.py b/app/lib/utils.py index f824ea0..d7b6d4c 100644 --- a/app/lib/utils.py +++ b/app/lib/utils.py @@ -7,9 +7,39 @@ import hashlib from app import app from distutils.version import StrictVersion - +from datetime import datetime,timedelta +from threading import Thread from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser + +idp_timestamp = datetime(1970,1,1) +idp_data = None +if app.config['SAML_ENABLED']: + idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL']) + if idp_data == None: + print('SAML: IDP Metadata initial load failed') + exit(-1) + idp_timestamp = datetime.now() + +def get_idp_data(): + global idp_data, idp_timestamp + lifetime = timedelta(minutes=app.config['SAML_METADATA_CACHE_LIFETIME']) + if idp_timestamp+lifetime < datetime.now(): + background_thread = Thread(target=retreive_idp_data) + background_thread.start() + return idp_data + +def retreive_idp_data(): + global idp_data, idp_timestamp + new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL']) + if new_idp_data != None: + idp_data = new_idp_data + idp_timestamp = datetime.now() + print("SAML: IDP Metadata successfully retreived from: " + app.config['SAML_METADATA_URL']) + else: + print("SAML: IDP Metadata could not be retreived") if 'TIMEOUT' in app.config.keys(): TIMEOUT = app.config['TIMEOUT'] @@ -179,5 +209,57 @@ def prepare_flask_request(request): } def init_saml_auth(req): - auth = OneLogin_Saml2_Auth(req, custom_base_path=app.config['SAML_PATH']) + own_url = '' + if req['https'] == 'on': + own_url = 'https://' + else: + own_url = 'http://' + own_url += req['http_host'] + metadata = get_idp_data() + settings = {} + settings['sp'] = {} + settings['sp']['NameIDFormat'] = idp_data['sp']['NameIDFormat'] + settings['sp']['entityId'] = app.config['SAML_SP_ENTITY_ID'] + settings['sp']['privateKey'] = '' + settings['sp']['x509cert'] = '' + settings['sp']['assertionConsumerService'] = {} + settings['sp']['assertionConsumerService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + settings['sp']['assertionConsumerService']['url'] = own_url+'/saml/authorized' + settings['sp']['attributeConsumingService'] = {} + settings['sp']['singleLogoutService'] = {} + settings['sp']['singleLogoutService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + settings['sp']['singleLogoutService']['url'] = own_url+'/saml/sls' + settings['idp'] = metadata['idp'] + settings['strict'] = True + settings['debug'] = app.config['SAML_DEBUG'] + settings['security'] = {} + settings['security']['digestAlgorithm'] = 'http://www.w3.org/2000/09/xmldsig#sha1' + settings['security']['metadataCacheDuration'] = None + settings['security']['metadataValidUntil'] = None + settings['security']['requestedAuthnContext'] = True + settings['security']['signatureAlgorithm'] = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + settings['security']['wantAssertionsEncrypted'] = False + settings['security']['wantAttributeStatement'] = True + settings['security']['wantNameId'] = True + settings['security']['authnRequestsSigned'] = False + settings['security']['logoutRequestSigned'] = False + settings['security']['logoutResponseSigned'] = False + settings['security']['nameIdEncrypted'] = False + settings['security']['signMetadata'] = False + settings['security']['wantAssertionsSigned'] = True + settings['security']['wantMessagesSigned'] = True + settings['security']['wantNameIdEncrypted'] = False + settings['contactPerson'] = {} + settings['contactPerson']['support'] = {} + settings['contactPerson']['support']['emailAddress'] = app.config['SAML_SP_CONTACT_NAME'] + settings['contactPerson']['support']['givenName'] = app.config['SAML_SP_CONTACT_MAIL'] + settings['contactPerson']['technical'] = {} + settings['contactPerson']['technical']['emailAddress'] = app.config['SAML_SP_CONTACT_NAME'] + settings['contactPerson']['technical']['givenName'] = app.config['SAML_SP_CONTACT_MAIL'] + settings['organization'] = {} + settings['organization']['en-US'] = {} + settings['organization']['en-US']['displayname'] = 'PowerDNS-Admin' + settings['organization']['en-US']['name'] = 'PowerDNS-Admin' + settings['organization']['en-US']['url'] = own_url + auth = OneLogin_Saml2_Auth(req, settings) return auth \ No newline at end of file diff --git a/config_template.py b/config_template.py index d2437f6..d1a5177 100644 --- a/config_template.py +++ b/config_template.py @@ -67,6 +67,15 @@ GITHUB_OAUTH_AUTHORIZE = 'http://127.0.0.1:5000/oauth/authorize' # SAML Authnetication SAML_ENABLED = False +SAML_DEBUG = True +SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml') +##Example for ADFS Metadata-URL +SAML_METADATA_URL = 'https:///FederationMetadata/2007-06/FederationMetadata.xml' +#Cache Lifetime in Seconds +SAML_METADATA_CACHE_LIFETIME = 1 +SAML_SP_ENTITY_ID = 'http://' +SAML_SP_CONTACT_NAME = '' +SAML_SP_CONTACT_MAIL = '' #Default Auth BASIC_ENABLED = True diff --git a/saml/template_advanced_settings.json b/saml/template_advanced_settings.json deleted file mode 100644 index 6bd7fb7..0000000 --- a/saml/template_advanced_settings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "security": { - "nameIdEncrypted": false, - "authnRequestsSigned": false, - "logoutRequestSigned": false, - "logoutResponseSigned": false, - "signMetadata": false, - "wantMessagesSigned": true, - "wantAssertionsSigned": true, - "wantNameIdEncrypted": false - }, - "contactPerson": { - "technical": { - "givenName": "", - "emailAddress": "" - }, - "support": { - "givenName" : "", - "emailAddress": "" - } - }, - "organization": { - "en-US": { - "name": "PowerDNS-Admin", - "displayname": "PowerDNS-Admin", - "url": "https://" - } - } -} \ No newline at end of file diff --git a/saml/template_settings.json b/saml/template_settings.json deleted file mode 100644 index 13aa758..0000000 --- a/saml/template_settings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "strict": true, - "debug": true, - "sp": { - "entityId": "http://", - "assertionConsumerService": { - "url": "https:///saml/authorized", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" - }, - "singleLogoutService": { - "url": "https:///saml/sls", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - }, - "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - "x509cert": "", - "privateKey": "" - }, - "idp": { - "entityId": "http:///adfs/services/trust", - "singleSignOnService": { - "url": "https:///adfs/ls/", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - }, - "singleLogoutService": { - "url": "https:///adfs/ls/", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - }, - "x509cert": "" - } -} \ No newline at end of file From 9a4021d5e5c828c99dec7bd966cabc00e4a561e8 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Fri, 20 Oct 2017 12:53:03 +0200 Subject: [PATCH 20/70] Add access control for non-admin users (cherry picked from commit 6e5b704) --- app/models.py | 7 ++++++ app/views.py | 70 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/app/models.py b/app/models.py index 9aee967..fc88c7e 100644 --- a/app/models.py +++ b/app/models.py @@ -314,6 +314,13 @@ class User(db.Model): user_domains.append(q[2]) return user_domains + def can_access_domain(self, domain_name): + if self.role.name == "Administrator": + return True + + query = self.get_domain_query().filter(Domain.name == domain_name) + return query.count() >= 1 + def delete(self): """ Delete a user diff --git a/app/views.py b/app/views.py index 8cc8761..05b0f29 100644 --- a/app/views.py +++ b/app/views.py @@ -296,36 +296,39 @@ def dashboard(): def domain(domain_name): r = Record() domain = Domain.query.filter(Domain.name == domain_name).first() - if domain: - # query domain info from PowerDNS API - zone_info = r.get_record_data(domain.name) - if zone_info: - jrecords = zone_info['records'] - else: - # can not get any record, API server might be down - return redirect(url_for('error', code=500)) - - records = [] - #TODO: This should be done in the "model" instead of "view" - if NEW_SCHEMA: - for jr in jrecords: - if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: - for subrecord in jr['records']: - record = Record(name=jr['name'], type=jr['type'], status='Disabled' if subrecord['disabled'] else 'Active', ttl=jr['ttl'], data=subrecord['content']) - records.append(record) - else: - for jr in jrecords: - if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: - record = Record(name=jr['name'], type=jr['type'], status='Disabled' if jr['disabled'] else 'Active', ttl=jr['ttl'], data=jr['content']) - records.append(record) - if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name): - editable_records = app.config['RECORDS_ALLOW_EDIT'] - else: - editable_records = ['PTR'] - return render_template('domain.html', domain=domain, records=records, editable_records=editable_records) - else: + if not domain: return redirect(url_for('error', code=404)) + if not current_user.can_access_domain(domain_name): + abort(403) + + # query domain info from PowerDNS API + zone_info = r.get_record_data(domain.name) + if zone_info: + jrecords = zone_info['records'] + else: + # can not get any record, API server might be down + return redirect(url_for('error', code=500)) + + records = [] + #TODO: This should be done in the "model" instead of "view" + if NEW_SCHEMA: + for jr in jrecords: + if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: + for subrecord in jr['records']: + record = Record(name=jr['name'], type=jr['type'], status='Disabled' if subrecord['disabled'] else 'Active', ttl=jr['ttl'], data=subrecord['content']) + records.append(record) + else: + for jr in jrecords: + if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: + record = Record(name=jr['name'], type=jr['type'], status='Disabled' if jr['disabled'] else 'Active', ttl=jr['ttl'], data=jr['content']) + records.append(record) + if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name): + editable_records = app.config['RECORDS_ALLOW_EDIT'] + else: + editable_records = ['PTR'] + return render_template('domain.html', domain=domain, records=records, editable_records=editable_records) + @app.route('/admin/domain/add', methods=['GET', 'POST']) @login_required @@ -416,6 +419,10 @@ def record_apply(domain_name): example jdata: {u'record_ttl': u'1800', u'record_type': u'CNAME', u'record_name': u'test4', u'record_status': u'Active', u'record_data': u'duykhanh.me'} """ #TODO: filter removed records / name modified records. + + if not current_user.can_access_domain(domain_name): + return make_response(jsonify({'status': 'error', 'msg': 'You do not have access to that domain'}), 403) + try: pdata = request.data jdata = json.loads(pdata) @@ -440,6 +447,10 @@ def record_update(domain_name): This route is used for domain work as Slave Zone only Pulling the records update from its Master """ + + if not current_user.can_access_domain(domain_name): + return make_response(jsonify({'status': 'error', 'msg': 'You do not have access to that domain'}), 403) + try: pdata = request.data jdata = json.loads(pdata) @@ -474,6 +485,9 @@ def record_delete(domain_name, record_name, record_type): @app.route('/domain//dnssec', methods=['GET']) @login_required def domain_dnssec(domain_name): + if not current_user.can_access_domain(domain_name): + return make_response(jsonify({'status': 'error', 'msg': 'You do not have access to that domain'}), 403) + domain = Domain() dnssec = domain.get_domain_dnssec(domain_name) return make_response(jsonify(dnssec), 200) From 37fee207a5484db29bd02e79451cdf6c17fc92eb Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 1 Nov 2017 22:30:08 +0100 Subject: [PATCH 21/70] marked google oauth users as external --- app/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views.py b/app/views.py index 4c99711..a2c4456 100644 --- a/app/views.py +++ b/app/views.py @@ -273,6 +273,7 @@ def login(): session['user_id'] = user.id login_user(user, remember = False) + session['external_auth'] = True return redirect(url_for('index')) if 'github_token' in session: From fc8bc2b2e766b028e18a22c86b675551518ed31c Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 1 Nov 2017 22:36:42 +0100 Subject: [PATCH 22/70] updated documentation --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2957ed3..7d402fe 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ PowerDNS Web-GUI - Built by Flask - Local / LDAP user authentication - Support Two-factor authentication (TOTP) - Support SAML authentication +- Google oauth authentication +- Github oauth authentication - User management - User access management based on domain - User activity logging @@ -86,7 +88,8 @@ Run the application and enjoy! ``` ### SAML Authentication -SAML authentication is supported. In order to use it you have to create your own settings.json and advanced_settings.json based on the templates. +SAML authentication is supported. Setting are retrieved from Metdata-XML. +Metadata URL is configured in config.py as well as caching interval. Following Assertions are supported and used by this application: - nameidentifier in form of email address as user login - email used as user email address From 17b820923ccaec8fb3ad279c92c89406a8dbb09f Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Thu, 2 Nov 2017 01:31:50 +0100 Subject: [PATCH 23/70] added basic travis-definition --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..307c6bb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: python +python: + - "2.6" + - "2.7" +install: + - pip install -r requirements.txt \ No newline at end of file From 483091bea738b3bdbdcba923b11b57aacf08df36 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Thu, 2 Nov 2017 01:38:51 +0100 Subject: [PATCH 24/70] added travis requirements --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 307c6bb..1babbe9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,8 @@ language: python python: - "2.6" - "2.7" +before_install: + - 'travis_retry sudo apt-get update' + - 'travis_retry sudo apt-get install python-dev libxml2-dev libxmlsec1-dev' install: - pip install -r requirements.txt \ No newline at end of file From 91758680f7e51c98459736968cb7d9cc0b6c281a Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Thu, 2 Nov 2017 02:15:33 +0100 Subject: [PATCH 25/70] added basic travis script --- .travis.yml | 4 +++- run_travis.sh | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 run_travis.sh diff --git a/.travis.yml b/.travis.yml index 1babbe9..aafa1b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,6 @@ before_install: - 'travis_retry sudo apt-get update' - 'travis_retry sudo apt-get install python-dev libxml2-dev libxmlsec1-dev' install: - - pip install -r requirements.txt \ No newline at end of file + - pip install -r requirements.txt +script: + - sh run_travis.sh \ No newline at end of file diff --git a/run_travis.sh b/run_travis.sh new file mode 100644 index 0000000..565f547 --- /dev/null +++ b/run_travis.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +python run.py& +nosetests --with-coverage \ No newline at end of file From 63632996dba9007d07edd73fcb6ced0b9cf1df7d Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Thu, 2 Nov 2017 02:32:51 +0100 Subject: [PATCH 26/70] updated travis and config_template --- .travis.yml | 2 ++ config_template.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index aafa1b1..f2b93e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,7 @@ before_install: - 'travis_retry sudo apt-get install python-dev libxml2-dev libxmlsec1-dev' install: - pip install -r requirements.txt +before_script: + - mv config_template.py config.py script: - sh run_travis.sh \ No newline at end of file diff --git a/config_template.py b/config_template.py index 62f7328..cdde7b4 100644 --- a/config_template.py +++ b/config_template.py @@ -28,10 +28,10 @@ SQLA_DB_HOST = 'mysqlhostorip' SQLA_DB_NAME = 'powerdnsadmin' #MySQL -SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'\ - +SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME +#SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'\ +# +SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME #SQLite -#SQLALCHEMY_DATABASE_URI = 'sqlite:////path/to/your/pdns.db' +SQLALCHEMY_DATABASE_URI = 'sqlite:///pdns.db' SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') SQLALCHEMY_TRACK_MODIFICATIONS = True From 6a47b1e4756636ef983db9b595984ba77ed3b9fc Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 2 Nov 2017 02:41:26 +0100 Subject: [PATCH 27/70] added travis status --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7d402fe..88d8ae4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # PowerDNS-Admin PowerDNS Web-GUI - Built by Flask +[![Build Status](https://travis-ci.org/thomasDOTde/PowerDNS-Admin.svg?branch=master)](https://travis-ci.org/thomasDOTde/PowerDNS-Admin) #### Features: - Multiple domain management From 9e719a3a9888c0cc2addd1d85287372656f86f26 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Fri, 3 Nov 2017 00:00:04 +0100 Subject: [PATCH 28/70] fixed merge --- app/models.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/app/models.py b/app/models.py index 7514854..a7f9872 100644 --- a/app/models.py +++ b/app/models.py @@ -288,28 +288,6 @@ class User(db.Model): else: logging.error('Unsupported authentication method') return False - # try to get user's firstname & lastname from LDAP - # this might be changed in the future - self.firstname = result[0][0][1]['givenName'][0] - self.lastname = result[0][0][1]['sn'][0] - self.email = result[0][0][1]['mail'][0] - except Exception: - self.firstname = self.username - self.lastname = '' - - # first register user will be in Administrator role - self.role_id = Role.query.filter_by(name='User').first().id - if User.query.count() == 0: - self.role_id = Role.query.filter_by(name='Administrator').first().id - - self.create_user() - logging.info('Created user "%s" in the DB' % self.username) - - return True - - logging.error('Unsupported authentication method') - return False - def create_user(self): """ From 54e61bf07235ba9b34d4401d0b76309ce84bd54b Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Fri, 3 Nov 2017 12:24:25 +0100 Subject: [PATCH 29/70] added custom error page for SAML authentication errors --- app/templates/errors/SAML.html | 45 ++++++++++++++++++++++++++++++++++ app/views.py | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 app/templates/errors/SAML.html diff --git a/app/templates/errors/SAML.html b/app/templates/errors/SAML.html new file mode 100644 index 0000000..07355d0 --- /dev/null +++ b/app/templates/errors/SAML.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% block title %}DNS Control Panel - SAML Authentication Error{% endblock %} + +{% block dashboard_stat %} + +
    +

    + SAML + Error +

    + +
    +{% endblock %} + +{% block content %} + +
    +
    +
    +

    SAML Authentication Error



    +
    +

    + Oops! Something went wrong +


    +

    + Login failed.
    + Error(s) when processing SAML Response:
    +

      + {% for error in errors %} +
    • {{ error }}
    • + {% endfor %} +
    + + You may return to the dashboard. +

    +
    + +
    + +
    + +{% endblock %} diff --git a/app/views.py b/app/views.py index a2c4456..120e25a 100644 --- a/app/views.py +++ b/app/views.py @@ -239,7 +239,7 @@ def saml_authorized(): login_user(user, remember=False) return redirect(url_for('index')) else: - return error(401,"an error occourred processing SAML response") + return render_template('errors/SAML.html', errors=errors) @app.route('/login', methods=['GET', 'POST']) @login_manager.unauthorized_handler From d65efe477a5996b5aec862affb2c52bc2ed21cc3 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Mon, 6 Nov 2017 23:36:11 +0100 Subject: [PATCH 30/70] ensure authentication isn't possible without password --- app/models.py | 4 +++- app/views.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/models.py b/app/models.py index a7f9872..17e52c0 100644 --- a/app/models.py +++ b/app/models.py @@ -133,7 +133,9 @@ class User(db.Model): def check_password(self, hashed_password): # Check hased password. Useing bcrypt, the salt is saved into the hash itself - return bcrypt.checkpw(self.plain_text_password.encode('utf-8'), hashed_password.encode('utf-8')) + if (self.plain_text_password): + return bcrypt.checkpw(self.plain_text_password.encode('utf-8'), hashed_password.encode('utf-8')) + return False def get_user_info_by_id(self): user_info = User.query.get(int(self.id)) diff --git a/app/views.py b/app/views.py index e624c16..09071f1 100644 --- a/app/views.py +++ b/app/views.py @@ -223,7 +223,7 @@ def saml_authorized(): if not user: # create user user = User(username=session['samlNameId'], - plain_text_password=gen_salt(30), + plain_text_password = None, email=session['samlNameId']) user.create_local_user() session['user_id'] = user.id @@ -233,7 +233,7 @@ def saml_authorized(): user.firstname = session['samlUserdata']["givenname"][0] if session['samlUserdata'].has_key("surname"): user.lastname = session['samlUserdata']["surname"][0] - user.plain_text_password = gen_salt(30) + user.plain_text_password = None user.update_profile() session['external_auth'] = True login_user(user, remember=False) @@ -267,7 +267,7 @@ def login(): user = User(username=email, firstname=first_name, lastname=surname, - plain_text_password=gen_salt(7), + plain_text_password=None, email=email) user.create_local_user() @@ -283,7 +283,7 @@ def login(): if not user: # create user user = User(username=user_info['name'], - plain_text_password=gen_salt(30), + plain_text_password=None, email=user_info['email']) user.create_local_user() From 971d6b2e28d3d06b10ada9ca3d3f54cea535d525 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Fri, 10 Nov 2017 12:28:42 +0100 Subject: [PATCH 31/70] fixed issue when not using LDAP --- app/models.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/models.py b/app/models.py index 17e52c0..5e65808 100644 --- a/app/models.py +++ b/app/models.py @@ -20,11 +20,6 @@ from lib import utils from lib.log import logger logging = logger('MODEL', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config() -LDAP_URI = app.config['LDAP_URI'] -LDAP_USERNAME = app.config['LDAP_USERNAME'] -LDAP_PASSWORD = app.config['LDAP_PASSWORD'] -LDAP_SEARCH_BASE = app.config['LDAP_SEARCH_BASE'] -LDAP_TYPE = app.config['LDAP_TYPE'] if 'LDAP_TYPE' in app.config.keys(): LDAP_URI = app.config['LDAP_URI'] LDAP_USERNAME = app.config['LDAP_USERNAME'] From 9855bc70dc65a2354f2e9ff868a1d7e1d34afb48 Mon Sep 17 00:00:00 2001 From: Radnik Date: Mon, 27 Nov 2017 11:02:21 +0100 Subject: [PATCH 32/70] Fixed iCheck for multiple pages --- app/templates/admin_manageuser.html | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/templates/admin_manageuser.html b/app/templates/admin_manageuser.html index 6b6ff64..6de6e5a 100644 --- a/app/templates/admin_manageuser.html +++ b/app/templates/admin_manageuser.html @@ -74,13 +74,16 @@ {% block extrascripts %} {% endblock %} {% block modals %} From 8c6a9346c08523d9b8fd6ee82d376c3fab5e10a8 Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Mon, 5 Mar 2018 14:50:33 +0100 Subject: [PATCH 53/70] Add domain to request --- app/templates/dashboard.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index eb75eb5..045626f 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -242,13 +242,13 @@ }); $(document.body).on("click", ".button_dnssec", function() { var domain = $(this).prop('id'); - getdnssec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec'); + getdnssec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec',domain); }); $(document.body).on("click", ".button_dnssec_enable", function() { var domain = $(this).prop('id'); enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/enable'); - + }); {% endblock %} From 197f555dfc9a11ab03bb489d0e93750b39646349 Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Mon, 5 Mar 2018 14:59:32 +0100 Subject: [PATCH 54/70] Add disable dnssec function --- app/templates/dashboard.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 045626f..481c5b8 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -250,6 +250,12 @@ enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/enable'); }); + + $(document.body).on("click", ".button_dnssec_disable", function() { + var domain = $(this).prop('id'); + enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/disable'); + + }); {% endblock %} {% block modals %} From 5ea70023ffbb79bdfbc578ec08b6ad3d65d27d6f Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Mon, 5 Mar 2018 15:06:40 +0100 Subject: [PATCH 55/70] remove dnssec keys --- app/models.py | 21 +++++++++++++++++++++ app/views.py | 8 +++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index ca35d70..d7de8c4 100644 --- a/app/models.py +++ b/app/models.py @@ -846,6 +846,27 @@ class Domain(db.Model): else: return {'status': 'error', 'msg': 'This domain doesnot exist'} + def delete_dnssec_key(self, domain_name, key_id): + """ + Remove keys DNSSEC + """ + domain = Domain.query.filter(Domain.name == domain_name).first() + if domain: + headers = {} + headers['X-API-Key'] = PDNS_API_KEY + url = '/servers/localhost/zones/%s/cryptokeys/%s' % (domain.name, key_id) + + try: + jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + url), headers=headers, method='DELETE') + if 'error' in jdata: + return {'status': 'error', 'msg': 'DNSSEC is not disabled for this domain', 'jdata' : jdata} + else: + return {'status': 'ok'} + except: + return {'status': 'error', 'msg': 'There was something wrong, please contact administrator','id': key_id, 'url': url} + else: + return {'status': 'error', 'msg': 'This domain doesnot exist'} + class DomainUser(db.Model): __tablename__ = 'domain_user' id = db.Column(db.Integer, primary_key = True) diff --git a/app/views.py b/app/views.py index 5a2d7df..2963354 100644 --- a/app/views.py +++ b/app/views.py @@ -662,7 +662,13 @@ def domain_dnssec_disable(domain_name): domain = Domain() dnssec = domain.get_domain_dnssec(domain_name) - return make_response(jsonify({'status': 'error', 'msg': 'Function not implemented'}), 400) + + for key in dnssec['dnssec']: + response = domain.delete_dnssec_key(domain_name,key['id']); + + return make_response(jsonify( { 'status': 'ok', 'msg': 'Setting updated.', 'response': response } )) + + #return make_response(jsonify({'status': 'error', 'msg': 'Function not implemented'}), 400) #return make_response(jsonify(dnssec), 200) @app.route('/domain//managesetting', methods=['GET', 'POST']) From c8d9f4bf22a157dcf356389befe1cdc08fe575a1 Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Mon, 5 Mar 2018 15:11:42 +0100 Subject: [PATCH 56/70] changes response --- app/views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/views.py b/app/views.py index 2963354..47e8ada 100644 --- a/app/views.py +++ b/app/views.py @@ -666,10 +666,8 @@ def domain_dnssec_disable(domain_name): for key in dnssec['dnssec']: response = domain.delete_dnssec_key(domain_name,key['id']); - return make_response(jsonify( { 'status': 'ok', 'msg': 'Setting updated.', 'response': response } )) + return make_response(jsonify( { 'status': 'ok', 'msg': 'DNSSEC removed.' } )) - #return make_response(jsonify({'status': 'error', 'msg': 'Function not implemented'}), 400) - #return make_response(jsonify(dnssec), 200) @app.route('/domain//managesetting', methods=['GET', 'POST']) @login_required From dcfa98ac59d4536769a807433d1695d9f0585432 Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Mon, 5 Mar 2018 15:26:45 +0100 Subject: [PATCH 57/70] Add disable button --- app/static/custom/js/custom.js | 46 ++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/app/static/custom/js/custom.js b/app/static/custom/js/custom.js index 4c597fa..0fdb3f8 100644 --- a/app/static/custom/js/custom.js +++ b/app/static/custom/js/custom.js @@ -1,3 +1,5 @@ +var dnssecKeyList = [] + function applyChanges(data, url, showResult, refreshPage) { var success = false; $.ajax({ @@ -131,7 +133,7 @@ function enable_dns_sec(url) { }) } -function getdnssec(url){ +function getdnssec(url,domain){ $.getJSON(url, function(data) { var modal = $("#modal_dnssec_info"); @@ -143,29 +145,35 @@ function getdnssec(url){ dnssec_msg = ''; var dnssec = data['dnssec']; - if (dnssec.length == 0 && PDNS_VERSION > 4.1) { + if (dnssec.length == 0 && parseFloat(PDNS_VERSION) > 4.1) { dnssec_msg = '

    Enable DNSSEC?'; modal.find('.modal-body p').html(dnssec_msg); - dnssec_footer = ''; + dnssec_footer = ''; modal.find('.modal-footer ').html(dnssec_footer); } - for (var i = 0; i < dnssec.length; i++) { - if (dnssec[i]['active']){ - dnssec_msg += '
    '+ - '

    '+dnssec[i]['keytype']+'

    '+ - 'DNSKEY'+ - ''+ - '
    '+ - '
    '; - if(dnssec[i]['ds']){ - var dsList = dnssec[i]['ds']; - dnssec_msg += 'DS'; - for (var j = 0; j < dsList.length; j++){ - dnssec_msg += ''; - } - } - dnssec_msg += ''; + else { + if (parseFloat(PDNS_VERSION) > 4.1) { + dnssec_footer = ''; + modal.find('.modal-footer ').html(dnssec_footer); } + for (var i = 0; i < dnssec.length; i++) { + if (dnssec[i]['active']){ + dnssec_msg += '
    '+ + '

    '+dnssec[i]['keytype']+'

    '+ + 'DNSKEY'+ + ''+ + '
    '+ + '
    '; + if(dnssec[i]['ds']){ + var dsList = dnssec[i]['ds']; + dnssec_msg += 'DS'; + for (var j = 0; j < dsList.length; j++){ + dnssec_msg += ''; + } + } + dnssec_msg += ''; + } + } } modal.find('.modal-body p').html(dnssec_msg); } From c1d33a83544d7ba4743dba572b5d3780cc9c86f3 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 28 Mar 2018 00:03:51 +0200 Subject: [PATCH 58/70] fix issue #19 --- app/lib/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/lib/utils.py b/app/lib/utils.py index 56fbf17..328d12a 100644 --- a/app/lib/utils.py +++ b/app/lib/utils.py @@ -10,14 +10,14 @@ from certutil import * from distutils.version import StrictVersion from datetime import datetime,timedelta from threading import Thread -from onelogin.saml2.auth import OneLogin_Saml2_Auth -from onelogin.saml2.utils import OneLogin_Saml2_Utils -from onelogin.saml2.settings import OneLogin_Saml2_Settings -from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser -idp_timestamp = datetime(1970,1,1) -idp_data = None if app.config['SAML_ENABLED']: + from onelogin.saml2.auth import OneLogin_Saml2_Auth + from onelogin.saml2.utils import OneLogin_Saml2_Utils + from onelogin.saml2.settings import OneLogin_Saml2_Settings + from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser + idp_timestamp = datetime(1970,1,1) + idp_data = None idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL']) if idp_data == None: print('SAML: IDP Metadata initial load failed') From 5ed8a33c7e14f2b6a49a2575823e89bd2b1fc902 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 28 Mar 2018 01:41:33 +0200 Subject: [PATCH 59/70] added feature requested in issue #28 --- app/models.py | 32 ++++++++++++++ app/templates/domain_management.html | 63 ++++++++++++++++++++++++++++ app/views.py | 24 +++++++++++ 3 files changed, 119 insertions(+) diff --git a/app/models.py b/app/models.py index d7de8c4..6b1cc99 100644 --- a/app/models.py +++ b/app/models.py @@ -676,6 +676,38 @@ class Domain(db.Model): logging.debug(str(e)) return {'status': 'error', 'msg': 'Cannot add this domain.'} + def update_soa_setting(self, domain_name, soa_edit_api): + domain = Domain.query.filter(Domain.name == domain_name).first() + if not domain: + return {'status': 'error', 'msg': 'Domain doesnt exist.'} + headers = {} + headers['X-API-Key'] = PDNS_API_KEY + if soa_edit_api == 'OFF': + post_data = { + "soa_edit_api": None, + "kind": domain.type + } + else: + post_data = { + "soa_edit_api": soa_edit_api, + "kind": domain.type + } + try: + jdata = utils.fetch_json( + urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain.name), headers=headers, + method='PUT', data=post_data) + if 'error' in jdata.keys(): + logging.error(jdata['error']) + return {'status': 'error', 'msg': jdata['error']} + else: + logging.info('soa-edit-api changed for domain %s successfully' % domain_name) + return {'status': 'ok', 'msg': 'soa-edit-api changed successfully'} + except Exception, e: + print traceback.format_exc() + logging.error('Cannot change soa-edit-api for domain %s' % domain_name) + logging.debug(str(e)) + return {'status': 'error', 'msg': 'Cannot change soa-edit-api this domain.'} + def create_reverse_domain(self, domain_name, domain_reverse_name): """ Check the existing reverse lookup domain, diff --git a/app/templates/domain_management.html b/app/templates/domain_management.html index a02caa9..b7bbf84 100644 --- a/app/templates/domain_management.html +++ b/app/templates/domain_management.html @@ -1,6 +1,18 @@ {% extends "base.html" %} {% block title %}DNS Control Panel - Domain Management{% endblock %} + {% block dashboard_stat %} + {% if status != None %} + {% if status['status'] == 'ok' %} +
    + Success! {{ status['msg'] }} +
    + {% else %} +
    + Error! {{ status['msg'] }} +
    + {% endif %} + {% endif %}

    Manage domain {{ domain.name }} @@ -86,6 +98,57 @@ +
    +
    +
    +
    +

    Change SOA-EDIT-API

    +
    +
    +

    The SOA-EDIT-API setting defines when and how the SOA serial number will be updated after a change is made to the domain.

    +
      +
    • + (OFF) - Not set +
    • +
    • + INCEPTION-INCREMENT - Uses YYYYMMDDSS format for SOA serial numbers. If the SOA serial from the backend is within two days after inception, it gets incremented by two (the backend should keep SS below 98). +
    • +
    • + INCEPTION - Sets the SOA serial to the last inception time in YYYYMMDD01 format. Uses localtime to find the day for inception time. Not recomended. +
    • +
    • + INCREMENT-WEEK - Sets the SOA serial to the number of weeks since the epoch, which is the last inception time in weeks. Not recomended. +
    • +
    • + INCREMENT-WEEKS - Increments the serial with the number of weeks since the UNIX epoch. This should work in every setup; but the result won't look like YYYYMMDDSS anymore. +
    • +
    • + EPOCH - Sets the SOA serial to the number of seconds since the epoch. +
    • +
    • + INCEPTION-EPOCH - Sets the new SOA serial number to the maximum of the old SOA serial number, and age in seconds of the last inception. +
    • +
    + New SOA-EDIT-API Setting: +
    +
    + +
    +
    +
    +
    +
    diff --git a/app/views.py b/app/views.py index 47e8ada..1f629e5 100644 --- a/app/views.py +++ b/app/views.py @@ -563,6 +563,30 @@ def domain_management(domain_name): return redirect(url_for('domain_management', domain_name=domain_name)) +@app.route('/admin/domain//change_soa_setting', methods=['POST']) +@login_required +@admin_role_required +def domain_change_soa_edit_api(domain_name): + domain = Domain.query.filter(Domain.name == domain_name).first() + if not domain: + return redirect(url_for('error', code=404)) + new_setting = request.form.get('soa_edit_api') + if new_setting == None: + return redirect(url_for('error', code=500)) + if new_setting == '0': + return redirect(url_for('domain_management', domain_name=domain_name)) + print new_setting + d = Domain() + status = d.update_soa_setting(domain_name=domain_name, soa_edit_api=new_setting) + if status['status'] != None: + users = User.query.all() + d = Domain(name=domain_name) + domain_user_ids = d.get_user() + return render_template('domain_management.html', domain=domain, users=users, domain_user_ids=domain_user_ids, + status=status) + else: + return redirect(url_for('error', code=500)) + @app.route('/domain//apply', methods=['POST'], strict_slashes=False) @login_required From c30cffd91c586af905244fa965a094b67aff4e8b Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 28 Mar 2018 01:52:48 +0200 Subject: [PATCH 60/70] fixed build issues. refactored PEP8 --- app/lib/utils.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/app/lib/utils.py b/app/lib/utils.py index 328d12a..32482b2 100644 --- a/app/lib/utils.py +++ b/app/lib/utils.py @@ -8,22 +8,23 @@ import hashlib from app import app from certutil import * from distutils.version import StrictVersion -from datetime import datetime,timedelta +from datetime import datetime, timedelta from threading import Thread if app.config['SAML_ENABLED']: - from onelogin.saml2.auth import OneLogin_Saml2_Auth - from onelogin.saml2.utils import OneLogin_Saml2_Utils - from onelogin.saml2.settings import OneLogin_Saml2_Settings - from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser - idp_timestamp = datetime(1970,1,1) - idp_data = None + from onelogin.saml2.auth import OneLogin_Saml2_Auth + from onelogin.saml2.utils import OneLogin_Saml2_Utils + from onelogin.saml2.settings import OneLogin_Saml2_Settings + from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser + idp_timestamp = datetime(1970, 1, 1) + idp_data = None idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL']) - if idp_data == None: + if idp_data is None: print('SAML: IDP Metadata initial load failed') exit(-1) idp_timestamp = datetime.now() + def get_idp_data(): global idp_data, idp_timestamp lifetime = timedelta(minutes=app.config['SAML_METADATA_CACHE_LIFETIME']) @@ -32,21 +33,24 @@ def get_idp_data(): background_thread.start() return idp_data + def retreive_idp_data(): global idp_data, idp_timestamp new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL']) - if new_idp_data != None: + if new_idp_data is not None: idp_data = new_idp_data idp_timestamp = datetime.now() print("SAML: IDP Metadata successfully retreived from: " + app.config['SAML_METADATA_URL']) else: print("SAML: IDP Metadata could not be retreived") + if 'TIMEOUT' in app.config.keys(): TIMEOUT = app.config['TIMEOUT'] else: TIMEOUT = 10 + def auth_from_url(url): auth = None parsed_url = urlparse.urlparse(url).netloc @@ -95,7 +99,8 @@ def fetch_remote(remote_url, method='GET', data=None, accept=None, params=None, def fetch_json(remote_url, method='GET', data=None, params=None, headers=None): - r = fetch_remote(remote_url, method=method, data=data, params=params, headers=headers, accept='application/json; q=1') + r = fetch_remote(remote_url, method=method, data=data, params=params, headers=headers, + accept='application/json; q=1') if method == "DELETE": return True @@ -126,6 +131,7 @@ def display_record_name(data): else: return record_name.replace('.'+domain_name, '') + def display_master_name(data): """ input data: "[u'127.0.0.1', u'8.8.8.8']" @@ -133,6 +139,7 @@ def display_master_name(data): matches = re.findall(r'\'(.+?)\'', data) return ", ".join(matches) + def display_time(amount, units='s', remove_seconds=True): """ Convert timestamp to normal time format @@ -173,6 +180,7 @@ def display_time(amount, units='s', remove_seconds=True): return final_string + def pdns_api_extended_uri(version): """ Check the pdns version @@ -182,15 +190,14 @@ def pdns_api_extended_uri(version): else: return "" + def email_to_gravatar_url(email, size=100): """ AD doesn't necessarily have email """ if not email: - email="" - - + email = "" hash_string = hashlib.md5(email).hexdigest() return "https://s.gravatar.com/avatar/%s?s=%s" % (hash_string, size) @@ -210,9 +217,10 @@ def prepare_flask_request(request): 'query_string': request.query_string } + def init_saml_auth(req): own_url = '' - if req['https'] == 'on': + if req['https'] is 'on': own_url = 'https://' else: own_url = 'http://' From f014798374cf745b54fa70627728afb98c4a0d31 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 28 Mar 2018 02:06:09 +0200 Subject: [PATCH 61/70] fixed ngoduykhanh/PowerDNS-Admin issue 194 --- app/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views.py b/app/views.py index 1f629e5..39002f6 100644 --- a/app/views.py +++ b/app/views.py @@ -64,7 +64,10 @@ def inject_default_domain_table_size_setting(): @app.context_processor def inject_auto_ptr_setting(): auto_ptr_setting = Setting.query.filter(Setting.name == 'auto_ptr').first() - return dict(auto_ptr_setting=strtobool(auto_ptr_setting.value)) + if auto_ptr_setting is None: + return dict(auto_ptr_setting=False) + else: + return dict(auto_ptr_setting=strtobool(auto_ptr_setting.value)) # START USER AUTHENTICATION HANDLER @app.before_request From 18d390eceabdda629034155f684316a5a9b3dd4d Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Wed, 28 Mar 2018 11:27:49 +0200 Subject: [PATCH 62/70] Moved to seperate directory --- docker-compose.yml | 50 ---------------------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index c9271b5..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,50 +0,0 @@ -version: '2' - -services: - - powerdns-authoritative: - image: winggundamth/powerdns-mysql:trusty - hostname: powerdns-authoritative - depends_on: - - powerdns-authoritative-mariadb - links: - - powerdns-authoritative-mariadb:mysqldb - ports: - - 172.17.0.1:53:53/udp - - 8081:8081 - environment: - - PDNS_DB_HOST=mysqldb - - PDNS_DB_USERNAME=root - - PDNS_DB_NAME=powerdns - - PDNS_DB_PASSWORD=PowerDNSPassword - - PDNS_API_KEY=PowerDNSAPIKey - - powerdns-authoritative-mariadb: - image: mariadb:10.1.15 - hostname: powerdns-authoritative-mariadb - environment: - - MYSQL_DATABASE=powerdns - - MYSQL_ROOT_PASSWORD=PowerDNSPassword - - powerdns-admin: - image: winggundamth/powerdns-admin:trusty - hostname: powerdns-admin - depends_on: - - powerdns-admin-mariadb - - powerdns-authoritative - links: - - powerdns-admin-mariadb:mysqldb - - powerdns-authoritative:powerdns-server - volumes: - - ./:/home/web/powerdns-admin - ports: - - 9393:9393 - environment: - - WAITFOR_DB=60 - - powerdns-admin-mariadb: - image: mariadb:10.1.15 - hostname: powerdns-admin-mariadb - environment: - - MYSQL_DATABASE=powerdns-admin - - MYSQL_ROOT_PASSWORD=PowerDNSAdminPassword From 7bceb1262fc0aa2e8d1bf477e6e7ff3b5726ed77 Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Wed, 28 Mar 2018 11:29:17 +0200 Subject: [PATCH 63/70] Added from upstream repo. Fixed missing Mysql client. --- docker/PowerDNS-MySQL/Dockerfile | 40 +++++++++ docker/PowerDNS-MySQL/build-files/pdns-pin | 3 + .../build-files/pdns.mysql.conf | 6 ++ docker/PowerDNS-MySQL/docker-entrypoint.sh | 89 +++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 docker/PowerDNS-MySQL/Dockerfile create mode 100644 docker/PowerDNS-MySQL/build-files/pdns-pin create mode 100644 docker/PowerDNS-MySQL/build-files/pdns.mysql.conf create mode 100755 docker/PowerDNS-MySQL/docker-entrypoint.sh diff --git a/docker/PowerDNS-MySQL/Dockerfile b/docker/PowerDNS-MySQL/Dockerfile new file mode 100644 index 0000000..a79ea40 --- /dev/null +++ b/docker/PowerDNS-MySQL/Dockerfile @@ -0,0 +1,40 @@ +# PowerDNS Authoritative Server with MySQL backend +# https://www.powerdns.com +# +# The PowerDNS Authoritative Server is the only solution that enables +# authoritative DNS service from all major databases, including but not limited +# to MySQL, PostgreSQL, SQLite3, Oracle, Sybase, Microsoft SQL Server, LDAP and +# plain text files. + +FROM winggundamth/ubuntu-base:trusty +MAINTAINER Jirayut Nimsaeng +ENV FROM_BASE=trusty-20160503.1 + +# 1) Add PowerDNS repository https://repo.powerdns.com +# 2) Install PowerDNS server +# 3) Clean to reduce Docker image size +ARG APT_CACHER_NG +COPY build-files /build-files +RUN [ -n "$APT_CACHER_NG" ] && \ + echo "Acquire::http::Proxy \"$APT_CACHER_NG\";" \ + > /etc/apt/apt.conf.d/11proxy || true; \ + apt-get update && \ + apt-get install -y curl && \ + curl https://repo.powerdns.com/FD380FBB-pub.asc | apt-key add - && \ + echo 'deb [arch=amd64] http://repo.powerdns.com/ubuntu trusty-auth-40 main' \ + > /etc/apt/sources.list.d/pdns-$(lsb_release -cs).list && \ + mv /build-files/pdns-pin /etc/apt/preferences.d/pdns && \ + apt-get update && \ + apt-get install -y pdns-server pdns-backend-mysql mysql-client && \ + mv /build-files/pdns.mysql.conf /etc/powerdns/pdns.d/pdns.mysql.conf && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /etc/apt/apt.conf.d/11proxy /build-files \ + /etc/powerdns/pdns.d/pdns.simplebind.conf + +# 1) Copy Docker entrypoint script +COPY docker-entrypoint.sh /docker-entrypoint.sh + +EXPOSE 53/udp 53 8081 +VOLUME ["/var/log", "/etc/powerdns"] +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["/usr/sbin/pdns_server", "--guardian=yes"] diff --git a/docker/PowerDNS-MySQL/build-files/pdns-pin b/docker/PowerDNS-MySQL/build-files/pdns-pin new file mode 100644 index 0000000..24d2bea --- /dev/null +++ b/docker/PowerDNS-MySQL/build-files/pdns-pin @@ -0,0 +1,3 @@ +Package: pdns-* +Pin: origin repo.powerdns.com +Pin-Priority: 600 diff --git a/docker/PowerDNS-MySQL/build-files/pdns.mysql.conf b/docker/PowerDNS-MySQL/build-files/pdns.mysql.conf new file mode 100644 index 0000000..42a99c8 --- /dev/null +++ b/docker/PowerDNS-MySQL/build-files/pdns.mysql.conf @@ -0,0 +1,6 @@ +launch+=gmysql +gmysql-port=3306 +gmysql-host=172.17.0.1 +gmysql-password=CHANGEME +gmysql-user=powerdns +gmysql-dbname=powerdns diff --git a/docker/PowerDNS-MySQL/docker-entrypoint.sh b/docker/PowerDNS-MySQL/docker-entrypoint.sh new file mode 100755 index 0000000..e508511 --- /dev/null +++ b/docker/PowerDNS-MySQL/docker-entrypoint.sh @@ -0,0 +1,89 @@ +#!/bin/sh +# Author: Jirayut 'Dear' Nimsaeng +# +set -e + +PDNS_CONF_PATH="/etc/powerdns/pdns.conf" +PDNS_MYSQL_CONF_PATH="/etc/powerdns/pdns.d/pdns.mysql.conf" +PDNS_MYSQL_HOST="localhost" +PDNS_MYSQL_PORT="3306" +PDNS_MYSQL_USERNAME="powerdns" +PDNS_MYSQL_PASSWORD="$PDNS_DB_PASSWORD" +PDNS_MYSQL_DBNAME="powerdns" + +if [ -z "$PDNS_DB_PASSWORD" ]; then + echo 'ERROR: PDNS_DB_PASSWORD environment variable not found' + exit 1 +fi + +# Configure variables +if [ "$PDNS_DB_HOST" ]; then + PDNS_MYSQL_HOST="$PDNS_DB_HOST" +fi +if [ "$PDNS_DB_PORT" ]; then + PDNS_MYSQL_PORT="$PDNS_DB_PORT" +fi +if [ "$PDNS_DB_USERNAME" ]; then + PDNS_MYSQL_USERNAME="$PDNS_DB_USERNAME" +fi +if [ "$PDNS_DB_NAME" ]; then + PDNS_MYSQL_DBNAME="$PDNS_DB_NAME" +fi + +# Configure mysql backend +sed -i \ + -e "s/^gmysql-host=.*/gmysql-host=$PDNS_MYSQL_HOST/g" \ + -e "s/^gmysql-port=.*/gmysql-port=$PDNS_MYSQL_PORT/g" \ + -e "s/^gmysql-user=.*/gmysql-user=$PDNS_MYSQL_USERNAME/g" \ + -e "s/^gmysql-password=.*/gmysql-password=$PDNS_MYSQL_PASSWORD/g" \ + -e "s/^gmysql-dbname=.*/gmysql-dbname=$PDNS_MYSQL_DBNAME/g" \ + $PDNS_MYSQL_CONF_PATH + +if [ "$PDNS_SLAVE" != "1" ]; then + # Configure to be master + sed -i \ + -e "s/^#\?\smaster=.*/master=yes/g" \ + -e "s/^#\?\sslave=.*/slave=no/g" \ + $PDNS_CONF_PATH +else + # Configure to be slave + sed -i \ + -e "s/^#\?\smaster=.*/master=no/g" \ + -e "s/^#\?\sslave=.*/slave=yes/g" \ + $PDNS_CONF_PATH +fi + +if [ "$PDNS_API_KEY" ]; then + # Enable API + sed -i \ + -e "s/^#\?\sapi=.*/api=yes/g" \ + -e "s!^#\?\sapi-logfile=.*!api-logfile=/dev/stdout!g" \ + -e "s/^#\?\sapi-key=.*/api-key=$PDNS_API_KEY/g" \ + -e "s/^#\?\swebserver=.*/webserver=yes/g" \ + -e "s/^#\?\swebserver-address=.*/webserver-address=0.0.0.0/g" \ + $PDNS_CONF_PATH +fi + +if [ "$PDNS_WEBSERVER_ALLOW_FROM" ]; then + sed -i \ + "s/^#\?\swebserver-allow-from=.*/webserver-allow-from=$PDNS_WEBSERVER_ALLOW_FROM/g" \ + $PDNS_CONF_PATH +fi + + +MYSQL_COMMAND="mysql -h $PDNS_MYSQL_HOST -P $PDNS_MYSQL_PORT -u $PDNS_MYSQL_USERNAME -p$PDNS_MYSQL_PASSWORD" + +until $MYSQL_COMMAND -e ";" ; do + >&2 echo "MySQL is unavailable - sleeping" + sleep 1 +done + +>&2 echo "MySQL is up - initial database if not exists" +MYSQL_CHECK_IF_HAS_TABLE="SELECT COUNT(DISTINCT table_name) FROM information_schema.columns WHERE table_schema = '$PDNS_MYSQL_DBNAME';" +MYSQL_NUM_TABLE=$($MYSQL_COMMAND --batch --skip-column-names -e "$MYSQL_CHECK_IF_HAS_TABLE") +if [ "$MYSQL_NUM_TABLE" -eq 0 ]; then + $MYSQL_COMMAND -D $PDNS_MYSQL_DBNAME < /usr/share/doc/pdns-backend-mysql/schema.mysql.sql +fi + +# Start PowerDNS +exec "$@" From 71faaf4d17ce085579473c10b8cd580baf060200 Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Wed, 28 Mar 2018 11:30:14 +0200 Subject: [PATCH 64/70] Updated version of the compose file. New image names, Port allocation. --- docker/docker-compose.yml | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docker/docker-compose.yml diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..a110c27 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,50 @@ +version: '2' + +services: + + powerdns-authoritative: + image: powerdns-mysql + hostname: powerdns-authoritative + depends_on: + - powerdns-authoritative-mariadb + links: + - powerdns-authoritative-mariadb:mysqldb + ports: + - 5553:53/udp + - 8081:8081 + environment: + - PDNS_DB_HOST=mysqldb + - PDNS_DB_USERNAME=root + - PDNS_DB_NAME=powerdns + - PDNS_DB_PASSWORD=PowerDNSPassword + - PDNS_API_KEY=PowerDNSAPIKey + + powerdns-authoritative-mariadb: + image: mariadb:10.1.15 + hostname: powerdns-authoritative-mariadb + environment: + - MYSQL_DATABASE=powerdns + - MYSQL_ROOT_PASSWORD=PowerDNSPassword + + powerdns-admin: + image: powerdns-admin + hostname: powerdns-admin + depends_on: + - powerdns-admin-mariadb + - powerdns-authoritative + links: + - powerdns-admin-mariadb:mysqldb + - powerdns-authoritative:powerdns-server + volumes: + - ../:/home/web/powerdns-admin + ports: + - 9393:9393 + environment: + - WAITFOR_DB=60 + + powerdns-admin-mariadb: + image: mariadb:10.1.15 + hostname: powerdns-admin-mariadb + environment: + - MYSQL_DATABASE=powerdns-admin + - MYSQL_ROOT_PASSWORD=PowerDNSAdminPassword From be4afd5ca64ee27ede614367e545a2534d5abf0a Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Wed, 28 Mar 2018 11:31:04 +0200 Subject: [PATCH 65/70] New docker image based on Alpine Linux --- docker/PowerDNS-Admin/Dockerfile | 42 ++++++++++++++++++++++ docker/PowerDNS-Admin/docker-entrypoint.sh | 12 +++++++ 2 files changed, 54 insertions(+) create mode 100644 docker/PowerDNS-Admin/Dockerfile create mode 100755 docker/PowerDNS-Admin/docker-entrypoint.sh diff --git a/docker/PowerDNS-Admin/Dockerfile b/docker/PowerDNS-Admin/Dockerfile new file mode 100644 index 0000000..133b709 --- /dev/null +++ b/docker/PowerDNS-Admin/Dockerfile @@ -0,0 +1,42 @@ +# PowerDNS-Admin +# Original from: +# https://github.com/ngoduykhanh/PowerDNS-Admin +# +# Initial image by winggundamth(/powerdns-mysql:trusty) +# +# +FROM alpine +MAINTAINER Jeroen Boonstra + +ENV APP_USER=web APP_NAME=powerdns-admin +ENV APP_PATH=/home/$APP_USER/$APP_NAME + + +RUN apk add --update \ + sudo \ + python \ + libxml2 \ + xmlsec \ + git \ + python-dev \ + py-pip \ + build-base \ + libxml2-dev \ + xmlsec-dev \ + libffi-dev \ + openldap-dev \ + && adduser -S web + +RUN sudo -u $APP_USER -H git clone --depth=1 \ + https://github.com/thomasDOTde/PowerDNS-Admin $APP_PATH + +RUN pip install -r $APP_PATH/requirements.txt +COPY docker-entrypoint.sh /docker-entrypoint.sh + + +USER $APP_USER +WORKDIR $APP_PATH +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["python", "run.py"] +EXPOSE 9393 +VOLUME ["/var/log"] diff --git a/docker/PowerDNS-Admin/docker-entrypoint.sh b/docker/PowerDNS-Admin/docker-entrypoint.sh new file mode 100755 index 0000000..58daa7a --- /dev/null +++ b/docker/PowerDNS-Admin/docker-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +set -e + +if [ "$WAITFOR_DB" -a ! -f "$APP_PATH/config.py" ]; then + cp "$APP_PATH/config_template_docker.py" "$APP_PATH/config.py" +fi + +cd $APP_PATH && python create_db.py + +# Start PowerDNS Admin +exec "$@" From 41b51733c8ab65a0cc17693d1a2d35e8b909b1ec Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Wed, 28 Mar 2018 11:31:18 +0200 Subject: [PATCH 66/70] Basic build script for containers. --- docker/build-images.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100755 docker/build-images.sh diff --git a/docker/build-images.sh b/docker/build-images.sh new file mode 100755 index 0000000..a9f759f --- /dev/null +++ b/docker/build-images.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +IMAGES=(PowerDNS-MySQL PowerDNS-Admin) +for IMAGE in "${IMAGES[@]}" + do + echo building $(basename $IMAGE | tr '[A-Z]' '[a-z]') + cd $IMAGE + docker build -t $(basename $IMAGE | tr '[A-Z]' '[a-z]') . + cd .. +done From cc12ae20ee848759e6d2df2d34fbf5f6cfaa0658 Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Wed, 28 Mar 2018 11:33:13 +0200 Subject: [PATCH 67/70] Add basic readme. --- docker/DOCKER.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docker/DOCKER.md diff --git a/docker/DOCKER.md b/docker/DOCKER.md new file mode 100644 index 0000000..c5dd6f0 --- /dev/null +++ b/docker/DOCKER.md @@ -0,0 +1,29 @@ +# Docker support +This is a updated version of the current docker support. +Container support is only for development purposes and should not be used in production without your own modificatins. + +It's not needed to reload the container after you make changes in your current branch. + +Images are currently not available in docker hub or other repository, so you have to build them yourself. + +After a successful launch PowerDNS-Admin is reachable at http://localhost:9393 + +PowerDNS runs op port localhost upd/5353 + + +## Basic commands: +### Build images +cd to this directory + +```# ./build-images.sh``` + +### Run containers +Build the images before you run this command. + +```# docker-compose up``` + +### Stop containers +```# docker-compose stop``` + +### Remove containers +```# docker-compose rm``` From 88c6d6ee33220a85f7e94d0e3ccfe52e4aa79b62 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 28 Mar 2018 11:43:54 +0200 Subject: [PATCH 68/70] missed to change one import for issue #19 --- app/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views.py b/app/views.py index 39002f6..b98df6b 100644 --- a/app/views.py +++ b/app/views.py @@ -19,9 +19,9 @@ from werkzeug.security import gen_salt from .models import User, Domain, Record, Server, History, Anonymous, Setting, DomainSetting from app import app, login_manager, github, google from lib import utils - -from onelogin.saml2.auth import OneLogin_Saml2_Auth -from onelogin.saml2.utils import OneLogin_Saml2_Utils +if app.config['SAML_ENABLED']: + from onelogin.saml2.auth import OneLogin_Saml2_Auth + from onelogin.saml2.utils import OneLogin_Saml2_Utils jinja2.filters.FILTERS['display_record_name'] = utils.display_record_name jinja2.filters.FILTERS['display_master_name'] = utils.display_master_name From 8b8d0420e2049039852b72a8124ed75f885f9a97 Mon Sep 17 00:00:00 2001 From: Jeroen Boonstra Date: Wed, 28 Mar 2018 12:10:12 +0200 Subject: [PATCH 69/70] fixed typo udp --- docker/DOCKER.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/DOCKER.md b/docker/DOCKER.md index c5dd6f0..196beba 100644 --- a/docker/DOCKER.md +++ b/docker/DOCKER.md @@ -8,7 +8,7 @@ Images are currently not available in docker hub or other repository, so you hav After a successful launch PowerDNS-Admin is reachable at http://localhost:9393 -PowerDNS runs op port localhost upd/5353 +PowerDNS runs op port localhost udp/5353 ## Basic commands: From f5a0052a060ab91ab993dcac8182335fbd0811fe Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Wed, 28 Mar 2018 14:19:48 +0200 Subject: [PATCH 70/70] fixed template for #28 --- app/templates/domain_management.html | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/templates/domain_management.html b/app/templates/domain_management.html index b7bbf84..8ca8507 100644 --- a/app/templates/domain_management.html +++ b/app/templates/domain_management.html @@ -2,14 +2,18 @@ {% block title %}DNS Control Panel - Domain Management{% endblock %} {% block dashboard_stat %} - {% if status != None %} - {% if status['status'] == 'ok' %} + {% if status %} + {% if status.get('status') == 'ok' %}
    - Success! {{ status['msg'] }} + Success! {{ status.get('msg') }}
    - {% else %} + {% elif status.get('status') == 'error' %}
    - Error! {{ status['msg'] }} + {% if status.get('msg') != None %} + Error! {{ status.get('msg') }} + {% else %} + Error! An undefined error occurred. + {% endif %}
    {% endif %} {% endif %}