Merge branch 'master' into feature-google-oauth

This commit is contained in:
Thomas 2017-11-01 22:18:43 +01:00 committed by GitHub
commit 83a0396350
11 changed files with 348 additions and 63 deletions

3
.gitignore vendored
View File

@ -23,6 +23,9 @@ nosetests.xml
flask flask
config.py config.py
logfile.log logfile.log
settings.json
advanced_settings.json
idp.crt
db_repository/* db_repository/*
upload/avatar/* upload/avatar/*

View File

@ -5,6 +5,7 @@ PowerDNS Web-GUI - Built by Flask
- Multiple domain management - Multiple domain management
- Local / LDAP user authentication - Local / LDAP user authentication
- Support Two-factor authentication (TOTP) - Support Two-factor authentication (TOTP)
- Support SAML authentication
- User management - User management
- User access management based on domain - User access management based on domain
- User activity logging - User activity logging
@ -84,6 +85,47 @@ Run the application and enjoy!
(flask)$ ./run.py (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 == "<here goes your source claim>"]
=> 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 ### Screenshots
![login page](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki/images/readme_screenshots/fullscreen-login.png?raw=true) ![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) ![dashboard](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki/images/readme_screenshots/fullscreen-dashboard.png?raw=true)

View File

@ -7,6 +7,39 @@ import hashlib
from app import app from app import app
from distutils.version import StrictVersion 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(): if 'TIMEOUT' in app.config.keys():
TIMEOUT = app.config['TIMEOUT'] TIMEOUT = app.config['TIMEOUT']
@ -159,3 +192,74 @@ def email_to_gravatar_url(email, size=100):
hash_string = hashlib.md5(email).hexdigest() hash_string = hashlib.md5(email).hexdigest()
return "https://s.gravatar.com/avatar/%s?s=%s" % (hash_string, size) 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

View File

@ -314,6 +314,13 @@ class User(db.Model):
user_domains.append(q[2]) user_domains.append(q[2])
return user_domains 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): def delete(self):
""" """
Delete a user Delete a user

View File

@ -167,7 +167,7 @@ json_library = {
return r + (pEnd || ''); return r + (pEnd || '');
}, },
prettyPrint: function(obj) { 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 jsonData = JSON.parse(obj);
var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg; var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
return JSON.stringify(jsonData, null, 3) return JSON.stringify(jsonData, null, 3)

View File

@ -324,6 +324,7 @@
record_data.val(data); record_data.val(data);
modal.modal('hide'); modal.modal('hide');
}) })
modal.modal('show');
} else if (record_type == "SOA") { } else if (record_type == "SOA") {
var modal = $("#modal_custom_record"); var modal = $("#modal_custom_record");
if (record_data.val() == "") { if (record_data.val() == "") {

View File

@ -101,11 +101,16 @@
{% if google_enabled %} {% if google_enabled %}
<a href="{{ url_for('google_login') }}">Google oauth login</a> <a href="{{ url_for('google_login') }}">Google oauth login</a>
{% endif %} {% endif %}
{% if saml_enabled %}
<br>
<a href="{{ url_for('saml_login') }}">SAML login</a>
{% endif %}
{% if github_enabled %} {% if github_enabled %}
<br>
<a href="{{ url_for('github_login') }}">Github oauth login</a> <a href="{{ url_for('github_login') }}">Github oauth login</a>
{% endif %} {% endif %}
<br>
{% if signup_enabled %} {% if signup_enabled %}
<br>
<a href="{{ url_for('register') }}" class="text-center">Create an account </a> <a href="{{ url_for('register') }}" class="text-center">Create an account </a>
{% endif %} {% endif %}
</div> </div>

View File

@ -19,7 +19,7 @@
<div class="col-lg-12"> <div class="col-lg-12">
<div class="box box-primary"> <div class="box box-primary">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">Edit my profile</h3> <h3 class="box-title">Edit my profile{% if external_account %} [Disabled - Authenticated externally]{% endif %}</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
<!-- Custom Tabs --> <!-- Custom Tabs -->
@ -29,10 +29,10 @@
Info</a></li> Info</a></li>
<li><a href="#tabs-avatar" data-toggle="tab">Change <li><a href="#tabs-avatar" data-toggle="tab">Change
Avatar</a></li> Avatar</a></li>
<li><a href="#tabs-password" data-toggle="tab">Change {% if not external_account %}<li><a href="#tabs-password" data-toggle="tab">Change
Password</a></li> Password</a></li>
<li><a href="#tabs-authentication" data-toggle="tab">Authentication <li><a href="#tabs-authentication" data-toggle="tab">Authentication
</a></li> </a></li>{% endif %}>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active" id="tabs-personal"> <div class="tab-pane active" id="tabs-personal">
@ -40,21 +40,21 @@
<div class="form-group"> <div class="form-group">
<label for="firstname">First Name</label> <input type="text" <label for="firstname">First Name</label> <input type="text"
class="form-control" name="firstname" id="firstname" class="form-control" name="firstname" id="firstname"
placeholder="{{ current_user.firstname }}"> placeholder="{{ current_user.firstname }}" {% if external_account %}disabled{% endif %}>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="lastname">Last Name</label> <input type="text" <label for="lastname">Last Name</label> <input type="text"
class="form-control" name="lastname" id="lastname" class="form-control" name="lastname" id="lastname"
placeholder="{{ current_user.lastname }}"> placeholder="{{ current_user.lastname }}" {% if external_account %}disabled{% endif %}>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="email">E-mail</label> <input type="text" <label for="email">E-mail</label> <input type="text"
class="form-control" name="email" id="email" class="form-control" name="email" id="email"
placeholder="{{ current_user.email }}"> placeholder="{{ current_user.email }}" {% if external_account %}disabled{% endif %}>
</div> </div>{% if not external_account %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Submit</button> <button type="submit" class="btn btn-flat btn-primary">Submit</button>
</div> </div>{% endif %}
</form> </form>
</div> </div>
<div class="tab-pane" id="tabs-avatar"> <div class="tab-pane" id="tabs-avatar">
@ -69,25 +69,25 @@
else %} <img else %} <img
src="{{ current_user.email|email_to_gravatar_url(size=200) }}" src="{{ current_user.email|email_to_gravatar_url(size=200) }}"
alt="" /> {% endif %} alt="" /> {% endif %}
</div> </div>{% if not external_account %}
<div> <div>
<label for="file">Select image</label> <input type="file" <label for="file">Select image</label> <input type="file"
id="file" name="file"> id="file" name="file">
</div> </div>{% endif %}
</div> </div>{% if not external_account %}
<div> <div>
<span class="label label-danger">NOTE! </span> <span>&nbsp;Only <span class="label label-danger">NOTE! </span> <span>&nbsp;Only
supports <strong>.PNG, .JPG, .JPEG</strong>. The best size supports <strong>.PNG, .JPG, .JPEG</strong>. The best size
to use is <strong>200x200</strong>. to use is <strong>200x200</strong>.
</span> </span>
</div> </div>{% endif %}
</div> </div>{% if not external_account %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Submit</button> <button type="submit" class="btn btn-flat btn-primary">Submit</button>
</div> </div>{% endif %}
</form> </form>
</div> </div>
<div class="tab-pane" id="tabs-password"> {% if not external_account %}<div class="tab-pane" id="tabs-password">
{% if not current_user.password %} Your account password is {% if not current_user.password %} Your account password is
managed via LDAP which isn't supported to change here. {% else managed via LDAP which isn't supported to change here. {% else
%} %}
@ -95,15 +95,15 @@
<div class="form-group"> <div class="form-group">
<label for="password">New Password</label> <input <label for="password">New Password</label> <input
type="password" class="form-control" name="password" type="password" class="form-control" name="password"
id="newpassword" /> id="newpassword" {% if external_account %}disabled{% endif %} />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="rpassword">Re-type New Password</label> <input <label for="rpassword">Re-type New Password</label> <input
type="password" class="form-control" name="rpassword" type="password" class="form-control" name="rpassword"
id="rpassword" /> id="rpassword" {% if external_account %}disabled{% endif %} />
</div> </div>
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Change <button type="submit" class="btn btn-flat btn-primary" {% if external_account %}disabled{% endif %}>Change
password</button> password</button>
</div> </div>
</form> </form>
@ -112,7 +112,7 @@
<div class="tab-pane" id="tabs-authentication"> <div class="tab-pane" id="tabs-authentication">
<form action="{{ user_profile }}" method="post"> <form action="{{ user_profile }}" method="post">
<div class="form-group"> <div class="form-group">
<input type="checkbox" id="otp_toggle" class="otp_toggle" {% if current_user.otp_secret %}checked{% endif %}> <input type="checkbox" id="otp_toggle" class="otp_toggle" {% if current_user.otp_secret %}checked{% endif %} {% if external_account %}disabled{% endif %}>
<label for="otp_toggle">Enable Two Factor Authentication</label> <label for="otp_toggle">Enable Two Factor Authentication</label>
{% if current_user.otp_secret %} {% if current_user.otp_secret %}
<div id="token_information"> <div id="token_information">
@ -124,7 +124,7 @@
{% endif %} {% endif %}
</div> </div>
</form> </form>
</div> </div>{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -20,6 +20,8 @@ from .models import User, Domain, Record, Server, History, Anonymous, Setting, D
from app import app, login_manager, github, google from app import app, login_manager, github, google
from lib import utils 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_record_name'] = utils.display_record_name
jinja2.filters.FILTERS['display_master_name'] = utils.display_master_name jinja2.filters.FILTERS['display_master_name'] = utils.display_master_name
@ -174,6 +176,71 @@ def github_login():
return abort(400) return abort(400)
return github.authorize(callback=url_for('authorized', _external=True)) 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']) @app.route('/login', methods=['GET', 'POST'])
@login_manager.unauthorized_handler @login_manager.unauthorized_handler
def login(): def login():
@ -184,6 +251,7 @@ def login():
SIGNUP_ENABLED = app.config['SIGNUP_ENABLED'] SIGNUP_ENABLED = app.config['SIGNUP_ENABLED']
GITHUB_ENABLE = app.config.get('GITHUB_OAUTH_ENABLE') GITHUB_ENABLE = app.config.get('GITHUB_OAUTH_ENABLE')
GOOGLE_ENABLE = app.config.get('GOOGLE_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: if g.user is not None and current_user.is_authenticated:
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
@ -214,11 +282,12 @@ def login():
if not user: if not user:
# create user # create user
user = User(username=user_info['name'], user = User(username=user_info['name'],
plain_text_password=gen_salt(7), plain_text_password=gen_salt(30),
email=user_info['email']) email=user_info['email'])
user.create_local_user() user.create_local_user()
session['user_id'] = user.id session['user_id'] = user.id
session['external_auth'] = True
login_user(user, remember = False) login_user(user, remember = False)
return redirect(url_for('index')) return redirect(url_for('index'))
@ -226,6 +295,7 @@ def login():
return render_template('login.html', return render_template('login.html',
github_enabled=GITHUB_ENABLE, github_enabled=GITHUB_ENABLE,
google_enabled=GOOGLE_ENABLE, google_enabled=GOOGLE_ENABLE,
saml_enabled=SAML_ENABLED,
ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE,
basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
@ -252,19 +322,38 @@ def login():
try: try:
auth = user.is_validate(method=auth_method) auth = user.is_validate(method=auth_method)
if auth == False: 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: except Exception, e:
error = e.message['desc'] if 'desc' in e.message else 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 # check if user enabled OPT authentication
if user.otp_secret: if user.otp_secret:
if otp_token: if otp_token:
good_token = user.verify_totp(otp_token) good_token = user.verify_totp(otp_token)
if not good_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: 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) login_user(user, remember = remember_me)
return redirect(request.args.get('next') or url_for('index')) return redirect(request.args.get('next') or url_for('index'))
@ -281,7 +370,9 @@ def login():
try: try:
result = user.create_local_user() result = user.create_local_user()
if result == True: 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: else:
return render_template('register.html', error=result) return render_template('register.html', error=result)
except Exception, e: except Exception, e:
@ -293,6 +384,7 @@ def logout():
session.pop('user_id', None) session.pop('user_id', None)
session.pop('github_token', None) session.pop('github_token', None)
session.pop('google_token', None) session.pop('google_token', None)
session.clear()
logout_user() logout_user()
return redirect(url_for('login')) return redirect(url_for('login'))
@ -326,36 +418,39 @@ def dashboard():
def domain(domain_name): def domain(domain_name):
r = Record() r = Record()
domain = Domain.query.filter(Domain.name == domain_name).first() domain = Domain.query.filter(Domain.name == domain_name).first()
if domain: if not 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:
return redirect(url_for('error', code=404)) 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']) @app.route('/admin/domain/add', methods=['GET', 'POST'])
@login_required @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'} 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. #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: try:
pdata = request.data pdata = request.data
jdata = json.loads(pdata) jdata = json.loads(pdata)
@ -470,6 +569,10 @@ def record_update(domain_name):
This route is used for domain work as Slave Zone only This route is used for domain work as Slave Zone only
Pulling the records update from its Master 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: try:
pdata = request.data pdata = request.data
jdata = json.loads(pdata) jdata = json.loads(pdata)
@ -504,6 +607,9 @@ def record_delete(domain_name, record_name, record_type):
@app.route('/domain/<string:domain_name>/dnssec', methods=['GET']) @app.route('/domain/<string:domain_name>/dnssec', methods=['GET'])
@login_required @login_required
def domain_dnssec(domain_name): 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() domain = Domain()
dnssec = domain.get_domain_dnssec(domain_name) dnssec = domain.get_domain_dnssec(domain_name)
return make_response(jsonify(dnssec), 200) return make_response(jsonify(dnssec), 200)
@ -701,8 +807,11 @@ def admin_settings_edit(setting):
@app.route('/user/profile', methods=['GET', 'POST']) @app.route('/user/profile', methods=['GET', 'POST'])
@login_required @login_required
def user_profile(): def user_profile():
if request.method == 'GET': external_account = False
return render_template('user_profile.html') 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': if request.method == 'POST':
# get new profile info # get new profile info
firstname = request.form['firstname'] if 'firstname' in request.form else '' 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 = 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() user.update_profile()
return render_template('user_profile.html') return render_template('user_profile.html', external_account=external_account)
@app.route('/user/avatar/<string:filename>') @app.route('/user/avatar/<string:filename>')

View File

@ -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_TOKEN = 'http://127.0.0.1:5000/oauth/token'
GITHUB_OAUTH_AUTHORIZE = 'http://127.0.0.1:5000/oauth/authorize' GITHUB_OAUTH_AUTHORIZE = 'http://127.0.0.1:5000/oauth/authorize'
# Google OAuth # Google OAuth
GOOGLE_OAUTH_ENABLE = False GOOGLE_OAUTH_ENABLE = False
GOOGLE_OAUTH_CLIENT_ID = ' ' GOOGLE_OAUTH_CLIENT_ID = ' '
@ -77,6 +78,18 @@ GOOGLE_TOKEN_PARAMS = {
GOOGLE_AUTHORIZE_URL='https://accounts.google.com/o/oauth2/auth' GOOGLE_AUTHORIZE_URL='https://accounts.google.com/o/oauth2/auth'
GOOGLE_BASE_URL='https://www.googleapis.com/oauth2/v1/' 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://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
#Cache Lifetime in Seconds
SAML_METADATA_CACHE_LIFETIME = 1
SAML_SP_ENTITY_ID = 'http://<SAML SP Entity ID>'
SAML_SP_CONTACT_NAME = '<contact name>'
SAML_SP_CONTACT_MAIL = '<contact mail>'
#Default Auth #Default Auth
BASIC_ENABLED = True BASIC_ENABLED = True
SIGNUP_ENABLED = True SIGNUP_ENABLED = True

View File

@ -1,6 +1,6 @@
Flask>=0.10 Flask==0.12.2
Flask-WTF>=0.11 Flask-WTF==0.14.2
Flask-Login>=0.2.11 Flask-Login==0.4.0
configobj==5.0.5 configobj==5.0.5
bcrypt==3.1.0 bcrypt==3.1.0
requests==2.7.0 requests==2.7.0
@ -12,3 +12,4 @@ pyotp==2.2.1
qrcode==5.3 qrcode==5.3
Flask-OAuthlib==0.9.3 Flask-OAuthlib==0.9.3
dnspython>=1.12.0 dnspython>=1.12.0
python-saml==2.3.0