diff --git a/.gitignore b/.gitignore index d548024..93d1207 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ nosetests.xml flask config.py logfile.log +settings.json +advanced_settings.json +idp.crt db_repository/* upload/avatar/* 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/app/lib/utils.py b/app/lib/utils.py index a4a2954..d7b6d4c 100644 --- a/app/lib/utils.py +++ b/app/lib/utils.py @@ -7,6 +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'] @@ -159,3 +192,74 @@ 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): + # 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(), + # 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): + 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/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/static/custom/js/custom.js b/app/static/custom/js/custom.js index 67f8f0f..ddd6d7b 100644 --- a/app/static/custom/js/custom.js +++ b/app/static/custom/js/custom.js @@ -167,7 +167,7 @@ json_library = { return r + (pEnd || ''); }, prettyPrint: function(obj) { - obj = obj.replace(/u'/g, "\'").replace(/'/g, "\"").replace(/(False|None)/g, "\"$1\""); + obj = obj.replace(/"/g, "\\\"").replace(/u'/g, "\'").replace(/'/g, "\"").replace(/(False|None)/g, "\"$1\""); var jsonData = JSON.parse(obj); var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg; return JSON.stringify(jsonData, null, 3) @@ -175,4 +175,4 @@ json_library = { .replace(//g, '>') .replace(jsonLine, json_library.replacer); } - }; \ No newline at end of file + }; diff --git a/app/templates/domain.html b/app/templates/domain.html index eeb4c27..89a2ce2 100644 --- a/app/templates/domain.html +++ b/app/templates/domain.html @@ -324,6 +324,7 @@ record_data.val(data); modal.modal('hide'); }) + modal.modal('show'); } else if (record_type == "SOA") { var modal = $("#modal_custom_record"); if (record_data.val() == "") { diff --git a/app/templates/login.html b/app/templates/login.html index ca32d04..5b26707 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -101,11 +101,16 @@ {% if google_enabled %} Google oauth login {% endif %} + {% if saml_enabled %} +
+ SAML login + {% endif %} {% if github_enabled %} +
Github oauth login {% endif %} -
{% if signup_enabled %} +
Create an account {% endif %} diff --git a/app/templates/user_profile.html b/app/templates/user_profile.html index 19201dc..28289ff 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 %}

@@ -29,10 +29,10 @@ Info
  • Change Avatar
  • -
  • Change + {% if not external_account %}
  • Change Password
  • Authentication -
  • + {% endif %}>
    @@ -40,21 +40,21 @@
    + 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 %}> +
    {% if not external_account %}
    -
    +
    {% endif %}
    @@ -69,25 +69,25 @@ else %} {% endif %} -
    +
    {% if not external_account %}
    -
    -
    + {% 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 %} @@ -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 %}
    @@ -124,7 +124,7 @@ {% endif %}
    -
    +
    {% endif %}
    diff --git a/app/views.py b/app/views.py index 2023d53..4c99711 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, google 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 @@ -174,6 +176,71 @@ 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) + 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') +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() + 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('/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() + errors = auth.get_errors() + 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(30), + 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(30) + user.update_profile() + session['external_auth'] = True + 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(): @@ -184,6 +251,7 @@ def login(): SIGNUP_ENABLED = app.config['SIGNUP_ENABLED'] GITHUB_ENABLE = app.config.get('GITHUB_OAUTH_ENABLE') GOOGLE_ENABLE = app.config.get('GOOGLE_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')) @@ -214,11 +282,12 @@ 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() session['user_id'] = user.id + session['external_auth'] = True login_user(user, remember = False) return redirect(url_for('index')) @@ -226,6 +295,7 @@ def login(): return render_template('login.html', github_enabled=GITHUB_ENABLE, google_enabled=GOOGLE_ENABLE, + saml_enabled=SAML_ENABLED, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) @@ -252,19 +322,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')) @@ -281,7 +370,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: @@ -293,6 +384,7 @@ def logout(): session.pop('user_id', None) session.pop('github_token', None) session.pop('google_token', None) + session.clear() logout_user() return redirect(url_for('login')) @@ -326,36 +418,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 @@ -446,6 +541,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) @@ -470,6 +569,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) @@ -504,6 +607,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) @@ -701,8 +807,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 '' @@ -737,7 +846,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/') diff --git a/config_template.py b/config_template.py index e6b5313..62f7328 100644 --- a/config_template.py +++ b/config_template.py @@ -65,6 +65,7 @@ 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' + # Google OAuth GOOGLE_OAUTH_ENABLE = False GOOGLE_OAUTH_CLIENT_ID = ' ' @@ -77,6 +78,18 @@ GOOGLE_TOKEN_PARAMS = { GOOGLE_AUTHORIZE_URL='https://accounts.google.com/o/oauth2/auth' GOOGLE_BASE_URL='https://www.googleapis.com/oauth2/v1/' +# 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 SIGNUP_ENABLED = True diff --git a/requirements.txt b/requirements.txt index 46ab6ba..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 @@ -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