mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-12-27 05:25:40 +00:00
Merge branch 'master' into feature-google-oauth
This commit is contained in:
commit
83a0396350
3
.gitignore
vendored
3
.gitignore
vendored
@ -23,6 +23,9 @@ nosetests.xml
|
||||
flask
|
||||
config.py
|
||||
logfile.log
|
||||
settings.json
|
||||
advanced_settings.json
|
||||
idp.crt
|
||||
|
||||
db_repository/*
|
||||
upload/avatar/*
|
||||
|
42
README.md
42
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 == "<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
|
||||
![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)
|
||||
|
104
app/lib/utils.py
104
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
|
@ -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
|
||||
|
@ -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(/>/g, '>')
|
||||
.replace(jsonLine, json_library.replacer);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -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() == "") {
|
||||
|
@ -101,11 +101,16 @@
|
||||
{% if google_enabled %}
|
||||
<a href="{{ url_for('google_login') }}">Google oauth login</a>
|
||||
{% endif %}
|
||||
{% if saml_enabled %}
|
||||
<br>
|
||||
<a href="{{ url_for('saml_login') }}">SAML login</a>
|
||||
{% endif %}
|
||||
{% if github_enabled %}
|
||||
<br>
|
||||
<a href="{{ url_for('github_login') }}">Github oauth login</a>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if signup_enabled %}
|
||||
<br>
|
||||
<a href="{{ url_for('register') }}" class="text-center">Create an account </a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<div class="col-lg-12">
|
||||
<div class="box box-primary">
|
||||
<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 class="box-body">
|
||||
<!-- Custom Tabs -->
|
||||
@ -29,10 +29,10 @@
|
||||
Info</a></li>
|
||||
<li><a href="#tabs-avatar" data-toggle="tab">Change
|
||||
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>
|
||||
<li><a href="#tabs-authentication" data-toggle="tab">Authentication
|
||||
</a></li>
|
||||
</a></li>{% endif %}>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="tabs-personal">
|
||||
@ -40,21 +40,21 @@
|
||||
<div class="form-group">
|
||||
<label for="firstname">First Name</label> <input type="text"
|
||||
class="form-control" name="firstname" id="firstname"
|
||||
placeholder="{{ current_user.firstname }}">
|
||||
placeholder="{{ current_user.firstname }}" {% if external_account %}disabled{% endif %}>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="lastname">Last Name</label> <input type="text"
|
||||
class="form-control" name="lastname" id="lastname"
|
||||
placeholder="{{ current_user.lastname }}">
|
||||
placeholder="{{ current_user.lastname }}" {% if external_account %}disabled{% endif %}>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">E-mail</label> <input type="text"
|
||||
class="form-control" name="email" id="email"
|
||||
placeholder="{{ current_user.email }}">
|
||||
</div>
|
||||
placeholder="{{ current_user.email }}" {% if external_account %}disabled{% endif %}>
|
||||
</div>{% if not external_account %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="tabs-avatar">
|
||||
@ -69,25 +69,25 @@
|
||||
else %} <img
|
||||
src="{{ current_user.email|email_to_gravatar_url(size=200) }}"
|
||||
alt="" /> {% endif %}
|
||||
</div>
|
||||
</div>{% if not external_account %}
|
||||
<div>
|
||||
<label for="file">Select image</label> <input type="file"
|
||||
id="file" name="file">
|
||||
</div>
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
</div>{% if not external_account %}
|
||||
<div>
|
||||
<span class="label label-danger">NOTE! </span> <span> Only
|
||||
supports <strong>.PNG, .JPG, .JPEG</strong>. The best size
|
||||
to use is <strong>200x200</strong>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
</div>{% if not external_account %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
</form>
|
||||
</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
|
||||
managed via LDAP which isn't supported to change here. {% else
|
||||
%}
|
||||
@ -95,15 +95,15 @@
|
||||
<div class="form-group">
|
||||
<label for="password">New Password</label> <input
|
||||
type="password" class="form-control" name="password"
|
||||
id="newpassword" />
|
||||
id="newpassword" {% if external_account %}disabled{% endif %} />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rpassword">Re-type New Password</label> <input
|
||||
type="password" class="form-control" name="rpassword"
|
||||
id="rpassword" />
|
||||
id="rpassword" {% if external_account %}disabled{% endif %} />
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@ -112,7 +112,7 @@
|
||||
<div class="tab-pane" id="tabs-authentication">
|
||||
<form action="{{ user_profile }}" method="post">
|
||||
<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>
|
||||
{% if current_user.otp_secret %}
|
||||
<div id="token_information">
|
||||
@ -124,7 +124,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
183
app/views.py
183
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/<string:domain_name>/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/<string:filename>')
|
||||
|
@ -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://<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
|
||||
BASIC_ENABLED = True
|
||||
SIGNUP_ENABLED = True
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user