Skip to content

Commit

Permalink
[CE-71] Add user authentication for react theme #close
Browse files Browse the repository at this point in the history
Support user login for react theme
Remove auth in nginx conf
Add login page for react theme

Change-Id: I17ad143766ccf37a70df1d1f76905b998c0d6021
Signed-off-by: Haitao Yue <hightall@me.com>
  • Loading branch information
hightall committed Jul 7, 2017
1 parent 274bee8 commit 19d1f2b
Show file tree
Hide file tree
Showing 29 changed files with 957 additions and 14 deletions.
4 changes: 1 addition & 3 deletions nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,8 @@ http {
return 444;
}

auth_basic "Login";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header Host $host:8080;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
}
Expand Down
8 changes: 8 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#
# SPDX-License-Identifier: Apache-2.0
#
import os
import bcrypt


class Config(object):
Expand All @@ -16,3 +18,9 @@ class ProductionConfig(Config):

class DevelopmentConfig(Config):
DEBUG = True
MONGODB_DB = os.getenv('MONGODB_DB', 'dashboard')
MONGODB_HOST = os.getenv('MONGODB_HOST', 'mongo')
MONGODB_PORT = int(os.getenv('MONGODB_PORT', 27017))
MONGODB_USERNAME = os.getenv('MONGODB_USERNAME', '')
MONGODB_PASSWORD = os.getenv('MONGODB_PASSWORD', '')
SALT = '$2b$12$e9UeM1mU0RahYaC4Ikn1Ce'
53 changes: 50 additions & 3 deletions src/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@

# Copyright IBM Corp, All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
import os
from common import log_handler, LOG_LEVEL
from flask import Flask, render_template
from flask import Flask, render_template, redirect, url_for
from resources import bp_index, \
bp_stat_view, bp_stat_api, \
bp_cluster_view, bp_cluster_api, \
bp_host_view, bp_host_api
bp_host_view, bp_host_api, bp_auth_api, bp_login
from mongoengine import connect
from flask_login import LoginManager, UserMixin, login_required
from resources.user import User
from resources import models
import bcrypt
import logging

logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
logger.addHandler(log_handler)

STATIC_FOLDER = os.getenv("STATIC_FOLDER", "themes/basic/static")
TEMPLATE_FOLDER = os.getenv("TEMPLATE_FOLDER", "themes/basic/templates")
Expand All @@ -19,6 +28,15 @@
app.config.from_object('config.DevelopmentConfig')
app.config.from_envvar('CELLO_CONFIG_FILE', silent=True)

connect(app.config.get("MONGODB_DB", "dashboard"),
host=app.config.get("MONGODB_HOST", "mongo"),
username=app.config.get("MONGODB_USERNAME", ""),
password=app.config.get("MONGODB_PASSWORD", ""),
connect=False, tz_aware=True)

login_manager = LoginManager()
login_manager.init_app(app)

app.logger.setLevel(LOG_LEVEL)
app.logger.addHandler(log_handler)

Expand All @@ -29,6 +47,18 @@
app.register_blueprint(bp_cluster_api)
app.register_blueprint(bp_stat_view)
app.register_blueprint(bp_stat_api)
app.register_blueprint(bp_auth_api)
app.register_blueprint(bp_login)

admin = os.environ.get("ADMIN", "admin")
admin_password = os.environ.get("ADMIN_PASSWORD", "pass")
salt = app.config.get("SALT", b"")
password = bcrypt.hashpw(admin_password.encode('utf8'), bytes(salt.encode()))
try:
user = User(admin, password, is_admin=True)
user.save()
except Exception:
pass


@app.errorhandler(404)
Expand All @@ -41,6 +71,23 @@ def internal_error(error):
return render_template('500.html'), 500


@login_manager.unauthorized_handler
def unauthorized_callback():
return redirect(url_for('bp_login.login'))


@login_manager.user_loader
def load_user(id):
if id is None:
redirect(url_for('bp_login.login'))
user = User()
user.get_by_id(id)
if user.is_active():
return user
else:
return None


if __name__ == '__main__':
app.run(
host='0.0.0.0',
Expand Down
3 changes: 3 additions & 0 deletions src/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ greenlet>=0.4.5,<=0.4.12
gunicorn>=19.0.0,<=19.6.0
pymongo>=3.2.0,<=3.4.0
requests>=2.0.0,<=2.13.0
mongoengine
flask-login
bcrypt
2 changes: 2 additions & 0 deletions src/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@
from .host_view import bp_host_view

from .stat import bp_stat_api, bp_stat_view
from .auth_api import bp_auth_api
from .login import bp_login
80 changes: 80 additions & 0 deletions src/resources/auth_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright IBM Corp, All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
import logging
import os
import sys
import bcrypt

from flask import Blueprint, redirect, url_for
from flask import request as r
from flask import current_app as app
from flask_login import login_user, logout_user

sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from common import log_handler, LOG_LEVEL, \
request_get, make_ok_resp, make_fail_resp, \
request_debug, request_json_body, \
CODE_CREATED, CODE_NOT_FOUND
from .user import User

logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
logger.addHandler(log_handler)

bp_auth_api = Blueprint('bp_auth_api', __name__,
url_prefix='/{}/{}'.format("api", "auth"))


@bp_auth_api.route('/register', methods=['POST'])
def register():
request_debug(r, logger)
if not r.form["username"] or not r.form["password"]:
error_msg = "register without enough data"
logger.warning(error_msg)
return make_fail_resp(error=error_msg, data=r.form)

username, password = r.form["username"], r.form["password"]
salt = app.config.get("SALT", b"")
password = bcrypt.hashpw(password.encode('utf8'), bytes(salt.encode()))

try:
user = User(username, password)
user.save()
return make_ok_resp(code=CODE_CREATED)
except Exception as exc:
logger.info("exc %s", exc)
return make_fail_resp(error="register failed")


@bp_auth_api.route('/login', methods=['POST'])
def login():
if not r.form["username"] or not r.form["password"]:
error_msg = "login without enough data"
logger.warning(error_msg)
return make_fail_resp(error=error_msg, data={'success': False})

username, password = r.form["username"], r.form["password"]
user_obj = User()
try:
user = user_obj.get_by_username_w_password(username)
if user.is_admin() and \
bcrypt.checkpw(password.encode('utf8'),
bytes(user.password.encode())):
login_user(user)
return make_ok_resp(data={'success': True,
'next': url_for('bp_index.show')},
code=CODE_CREATED)
else:
return make_fail_resp(error="login failed",
data={'success': False})
except Exception:
return make_fail_resp(error="login failed", data={'success': False})


@bp_auth_api.route('/logout', methods=['GET'])
def logout():
logout_user()
return make_ok_resp(data={'success': True,
'next': url_for('bp_login.login')})
5 changes: 5 additions & 0 deletions src/resources/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
request_debug, \
CLUSTER_LOG_TYPES, CLUSTER_LOG_LEVEL
from version import version, homepage, author
from flask_login import login_required, current_user

logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
Expand All @@ -29,6 +30,7 @@

@bp_index.route('/', methods=['GET'])
@bp_index.route('/index', methods=['GET'])
@login_required
def show():
request_debug(r, logger)
hosts = list(host_handler.list(filter_data={}))
Expand All @@ -46,6 +48,7 @@ def show():

clusters_temp = len(list(cluster_handler.list(filter_data={
"user_id": "/^__/"}, col_name="active")))
username, is_admin = current_user.username, current_user.isAdmin

return render_template("index.html", hosts=hosts,
hosts_free=hosts_free,
Expand All @@ -64,6 +67,8 @@ def show():
host_types=WORKER_TYPES,
log_types=CLUSTER_LOG_TYPES,
log_levels=CLUSTER_LOG_LEVEL,
username=username,
is_admin=is_admin
)


Expand Down
28 changes: 28 additions & 0 deletions src/resources/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

# Copyright IBM Corp, All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
import logging
import os
import sys
from flask import Blueprint, render_template
from flask import request as r

sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from common import log_handler, LOG_LEVEL, NETWORK_TYPES, CONSENSUS_PLUGINS, \
CONSENSUS_MODES, WORKER_TYPES, NETWORK_SIZE_FABRIC_PRE_V1, request_debug, \
CLUSTER_LOG_TYPES, CLUSTER_LOG_LEVEL

logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
logger.addHandler(log_handler)

bp_login = Blueprint('bp_login', __name__)


@bp_login.route('/login', methods=['GET'])
def login():
request_debug(r, logger)

return render_template("login.html")
15 changes: 15 additions & 0 deletions src/resources/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import sys
import os
import datetime
from mongoengine import Document, StringField,\
BooleanField, DateTimeField

sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))


class User(Document):
username = StringField(unique=True)
password = StringField(default=True)
active = BooleanField(default=True)
isAdmin = BooleanField(default=False)
timestamp = DateTimeField(default=datetime.datetime.now())
81 changes: 81 additions & 0 deletions src/resources/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import sys
import os
import logging
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from flask_login import UserMixin, AnonymousUserMixin
from resources import models
from common import log_handler, LOG_LEVEL

logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
logger.addHandler(log_handler)


class User(UserMixin):
def __init__(self, username=None, password=None, active=True,
is_admin=False, id=None):
self.username = username
self.password = password
self.active = active
self.isAdmin = is_admin
self.id = None

def is_active(self):
return self.active

def is_admin(self):
return self.isAdmin

def save(self):
new_user = models.User(username=self.username,
password=self.password,
active=self.active,
isAdmin=self.isAdmin)
new_user.save()
self.id = new_user.id
return self.id

def get_by_username(self, username):

dbUser = models.User.objects.get(username=username)
if dbUser:
self.username = dbUser.username
self.active = dbUser.active
self.id = dbUser.id
return self
else:
return None

def get_by_username_w_password(self, username):
try:
dbUser = models.User.objects.get(username=username)

if dbUser:
logger.info("get user")
self.username = dbUser.username
self.active = dbUser.active
self.password = dbUser.password
self.id = dbUser.id
self.isAdmin = dbUser.isAdmin
return self
else:
logger.info("not get user")
return None
except Exception as exc:
logger.info("get user exc %s", exc)
return None

def get_by_id(self, id):
dbUser = models.User.objects.with_id(id)
if dbUser:
self.username = dbUser.username
self.active = dbUser.active
self.id = dbUser.id

return self
else:
return None


class Anonymous(AnonymousUserMixin):
name = u"Anonymous"
2 changes: 1 addition & 1 deletion src/themes/react/static/js/components/layout/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function Header ({ user, logout, switchSider, siderFold, isNavbar, menuPopoverVi
<SubMenu style={{
float: 'right',
}} title={<span> <Icon type="user" />
{user.name} </span>}
{window.username} </span>}
>
<Menu.Item key="logout">
<a>logout</a>
Expand Down
6 changes: 2 additions & 4 deletions src/themes/react/static/js/models/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,8 @@ export default {
payload,
}, { call, put }) {
const data = yield call(logout, parse(payload))
if (data.success) {
yield put({
type: 'logoutSuccess',
})
if (data && data.data.success) {
window.location.href = data.data.next;
}
},
*switchSider ({
Expand Down

0 comments on commit 19d1f2b

Please sign in to comment.