Add initial code for user authentication
Change-Id: Ie0c439244f1ae3af707b73ef64b1a411c2aede20
This commit is contained in:
parent
1bbdf4a918
commit
4c69d120cd
@ -88,6 +88,10 @@ def start(csv_dir, compass_url):
|
||||
p.join()
|
||||
|
||||
progress_file = '/'.join((csv_dir, 'progress.csv'))
|
||||
write_progress_to_file(results_q, progress_file)
|
||||
|
||||
|
||||
def write_progress_to_file(results_q, progress_file):
|
||||
cluster_headers = ['cluster_id', 'progress_url']
|
||||
host_headers = ['host_id', 'progress_url']
|
||||
|
||||
|
@ -39,6 +39,7 @@ from compass.db.model import Machine
|
||||
from compass.db.model import Role
|
||||
from compass.db.model import Switch
|
||||
from compass.db.model import SwitchConfig
|
||||
from compass.db.model import User
|
||||
from compass.tasks.client import celery
|
||||
from compass.utils import flags
|
||||
from compass.utils import logsetting
|
||||
@ -93,6 +94,7 @@ TABLE_MAPPING = {
|
||||
'cluster': Cluster,
|
||||
'clusterhost': ClusterHost,
|
||||
'logprogressinghistory': LogProgressingHistory,
|
||||
'user': User
|
||||
}
|
||||
|
||||
|
||||
|
@ -13,14 +13,35 @@
|
||||
# limitations under the License.
|
||||
|
||||
__all__ = ['Flask', 'SQLAlchemy', 'compass_api']
|
||||
import datetime
|
||||
import jinja2
|
||||
import os
|
||||
|
||||
from compass.db.model import SECRET_KEY
|
||||
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
from flask.ext.login import LoginManager
|
||||
from flask import Flask
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.debug = True
|
||||
|
||||
app.secret_key = SECRET_KEY
|
||||
app.config['AUTH_HEADER_NAME'] = 'X-Auth-Token'
|
||||
app.config['REMEMBER_COOKIE_DURATION'] = datetime.timedelta(minutes=30)
|
||||
|
||||
templates_dir = "/".join((os.path.dirname(
|
||||
os.path.dirname(os.path.realpath(__file__))),
|
||||
'templates/'))
|
||||
|
||||
template_loader = jinja2.ChoiceLoader([
|
||||
app.jinja_loader,
|
||||
jinja2.FileSystemLoader(templates_dir),
|
||||
])
|
||||
|
||||
app.jinja_loader = template_loader
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'login'
|
||||
login_manager.init_app(app)
|
||||
|
||||
from compass.api import api as compass_api
|
||||
|
@ -17,14 +17,23 @@ import logging
|
||||
import netaddr
|
||||
import re
|
||||
import simplejson as json
|
||||
import sys
|
||||
|
||||
from flask import flash
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import session as app_session
|
||||
from flask import url_for
|
||||
|
||||
from flask.ext.restful import Resource
|
||||
from flask import request
|
||||
from sqlalchemy.sql import and_
|
||||
from sqlalchemy.sql import or_
|
||||
|
||||
from compass.api import app
|
||||
from compass.api import auth
|
||||
from compass.api import errors
|
||||
from compass.api import login_manager
|
||||
from compass.api import util
|
||||
from compass.db import database
|
||||
from compass.db.model import Adapter
|
||||
@ -36,8 +45,129 @@ from compass.db.model import Machine as ModelMachine
|
||||
from compass.db.model import Role
|
||||
from compass.db.model import Switch as ModelSwitch
|
||||
from compass.db.model import SwitchConfig
|
||||
from compass.db.model import User as ModelUser
|
||||
from compass.tasks.client import celery
|
||||
|
||||
from flask.ext.login import current_user
|
||||
from flask.ext.login import login_required
|
||||
from flask.ext.login import login_user
|
||||
from flask.ext.login import logout_user
|
||||
|
||||
from flask.ext.wtf import Form
|
||||
from wtforms.fields import BooleanField
|
||||
from wtforms.fields import PasswordField
|
||||
from wtforms.fields import TextField
|
||||
from wtforms.validators import Required
|
||||
|
||||
|
||||
@login_manager.header_loader
|
||||
def load_user_from_token(token):
|
||||
"""Return a user object from token."""
|
||||
duration = app.config['REMEMBER_COOKIE_DURATION']
|
||||
max_age = 0
|
||||
if sys.version_info > (2, 6):
|
||||
max_age = duration.total_seconds()
|
||||
else:
|
||||
max_age = (duration.microseconds + (
|
||||
duration.seconds + duration.days * 24 * 3600) * 1e6) / 1e6
|
||||
|
||||
user_id = auth.get_user_info_from_token(token, max_age)
|
||||
if not user_id:
|
||||
logging.info("No user can be found from the token!")
|
||||
return None
|
||||
|
||||
user = User.query.filter_by(id=user_id)
|
||||
return user
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
"""Load user from user ID."""
|
||||
return ModelUser.query.get(user_id)
|
||||
|
||||
|
||||
@app.route('/restricted')
|
||||
def restricted():
|
||||
return render_template('restricted.jinja')
|
||||
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden_403(exception):
|
||||
"""Unathenticated user page."""
|
||||
return render_template('forbidden.jinja'), 403
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
"""User logout."""
|
||||
logout_user()
|
||||
flash('You have logged out!')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Index page."""
|
||||
return render_template('index.jinja')
|
||||
|
||||
|
||||
@app.route('/token', methods=['POST'])
|
||||
def get_token():
|
||||
"""Get token from email and passowrd after user authentication."""
|
||||
data = json.loads(request.data)
|
||||
email = data['email']
|
||||
password = data['password']
|
||||
|
||||
user = auth.authenticate_user(email, password)
|
||||
if not user:
|
||||
error_msg = "User cannot be found or email and password do not match!"
|
||||
return errors.handle_invalid_user_info(
|
||||
errors.ObjectDoesNotExist(error_msg)
|
||||
)
|
||||
|
||||
token = user.get_auth_token()
|
||||
login_user(user)
|
||||
|
||||
return util.make_json_response(
|
||||
200, {"status": "OK", "token": token}
|
||||
)
|
||||
|
||||
|
||||
class LoginForm(Form):
|
||||
"""Define login form."""
|
||||
email = TextField('Email', validators=[Required()])
|
||||
password = PasswordField('Password', validators=[Required()])
|
||||
remember = BooleanField('Remember me', default=False)
|
||||
|
||||
|
||||
@app.route("/login", methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""User login."""
|
||||
if current_user.is_authenticated():
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
email = form.email.data
|
||||
password = form.password.data
|
||||
|
||||
user = auth.authenticate_user(email, password)
|
||||
if not user:
|
||||
flash('Wrong username or password!', 'error')
|
||||
return render_template('login.jinja', form=form)
|
||||
|
||||
if login_user(user, remember=form.remember.data):
|
||||
# Enable session expiration if user didnot choose to be
|
||||
# remembered.
|
||||
app_session.permanent = not form.remember.data
|
||||
flash('Logged in successfully!', 'success')
|
||||
return redirect(request.args.get('next') or url_for('index'))
|
||||
else:
|
||||
flash('This username is disabled!', 'error')
|
||||
|
||||
return render_template('login.jinja', form=form)
|
||||
|
||||
|
||||
class SwitchList(Resource):
|
||||
"""Query details of switches and poll swithes."""
|
||||
@ -594,9 +724,7 @@ class Cluster(Resource):
|
||||
|
||||
if is_valid:
|
||||
column = resources[resource]['column']
|
||||
session.query(
|
||||
ModelCluster
|
||||
).filter_by(id=cluster_id).update(
|
||||
session.query(ModelCluster).filter_by(id=cluster_id).update(
|
||||
{column: json.dumps(value)}
|
||||
)
|
||||
else:
|
||||
@ -1320,6 +1448,34 @@ class DashboardLinks(Resource):
|
||||
)
|
||||
|
||||
|
||||
class User(Resource):
|
||||
ENDPOINT = '/users'
|
||||
|
||||
@login_required
|
||||
def get(self, user_id):
|
||||
"""Get user's information for specified ID."""
|
||||
resp = {}
|
||||
with database.session() as session:
|
||||
user = session.query(ModelUser).filter_by(id=user_id).first()
|
||||
|
||||
if not user:
|
||||
error_msg = "Cannot find the User which id is %s" % user_id
|
||||
return errors.handle_not_exist(
|
||||
errors.ObjectDoesNotExist(error_msg)
|
||||
)
|
||||
resp['id'] = user_id
|
||||
resp['email'] = user.email
|
||||
resp['link'] = {
|
||||
"href": "/".join((self.ENDPOINT, str(user_id))),
|
||||
"rel": "self"
|
||||
}
|
||||
|
||||
return util.make_json_response(
|
||||
200, {"status": "OK",
|
||||
"user": resp}
|
||||
)
|
||||
|
||||
|
||||
TABLES = {
|
||||
'switch_config': {
|
||||
'name': SwitchConfig,
|
||||
@ -1359,6 +1515,7 @@ TABLES = {
|
||||
}
|
||||
|
||||
|
||||
@login_required
|
||||
@app.route("/export/<string:tname>", methods=['GET'])
|
||||
def export_csv(tname):
|
||||
"""export to csv file."""
|
||||
@ -1438,6 +1595,9 @@ util.add_resource(HostInstallingProgress,
|
||||
util.add_resource(ClusterInstallingProgress,
|
||||
'/clusters/<int:cluster_id>/progress')
|
||||
util.add_resource(DashboardLinks, '/dashboardlinks')
|
||||
util.add_resource(User,
|
||||
'/users',
|
||||
'/users/<int:user_id>')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
47
compass/api/auth.py
Normal file
47
compass/api/auth.py
Normal file
@ -0,0 +1,47 @@
|
||||
# Copyright 2014 Huawei Technologies Co. Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from itsdangerous import BadData
|
||||
import logging
|
||||
|
||||
from compass.db.model import login_serializer
|
||||
from compass.db.model import User
|
||||
|
||||
|
||||
def get_user_info_from_token(token, max_age):
|
||||
"""Return user's ID and hased password from token."""
|
||||
|
||||
user_id = None
|
||||
try:
|
||||
user_id = login_serializer.loads(token, max_age=max_age)
|
||||
|
||||
except BadData as err:
|
||||
logging.error("[auth][get_user_info_from_token] Exception: %s", err)
|
||||
return None
|
||||
|
||||
return user_id
|
||||
|
||||
|
||||
def authenticate_user(email, pwd):
|
||||
"""Authenticate a use by email and password."""
|
||||
|
||||
try:
|
||||
user = User.query.filter_by(email=email).first()
|
||||
if user and user.valid_password(pwd):
|
||||
return user
|
||||
except Exception as err:
|
||||
print '[auth][authenticate_user]Exception: %s' % err
|
||||
logging.info('[auth][authenticate_user]Exception: %s', err)
|
||||
|
||||
return None
|
@ -67,6 +67,13 @@ class MethodNotAllowed(Exception):
|
||||
return repr(self.message)
|
||||
|
||||
|
||||
class InvalidUserInfo(Exception):
|
||||
"""Define the Exception for incorrect user information."""
|
||||
def __init__(self, message):
|
||||
super(InvalidUserInfo, self).__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
@app.errorhandler(ObjectDoesNotExist)
|
||||
def handle_not_exist(error, failed_objs=None):
|
||||
"""Handler of ObjectDoesNotExist Exception."""
|
||||
@ -128,3 +135,11 @@ def handle_not_allowed_method(error):
|
||||
"message": "The method is not allowed to use"
|
||||
}
|
||||
return util.make_json_response(405, message)
|
||||
|
||||
|
||||
@app.errorhandler(InvalidUserInfo)
|
||||
def handle_invalid_user_info(error):
|
||||
"""Handler of InvalidUserInfo Exception."""
|
||||
message = {"status": "Incorrect User Info",
|
||||
"message": error.message}
|
||||
return util.make_json_response(401, message)
|
||||
|
@ -29,7 +29,7 @@ API = Api(app)
|
||||
def make_json_response(status_code, data):
|
||||
"""Wrap json format to the reponse object."""
|
||||
|
||||
result = json.dumps(data, indent=4)
|
||||
result = json.dumps(data, indent=4) + '\r\n'
|
||||
resp = make_response(result, status_code)
|
||||
resp.headers['Content-type'] = 'application/json'
|
||||
return resp
|
||||
|
@ -29,6 +29,9 @@ ENGINE = create_engine(setting.SQLALCHEMY_DATABASE_URI, convert_unicode=True)
|
||||
SESSION = sessionmaker(autocommit=False, autoflush=False)
|
||||
SESSION.configure(bind=ENGINE)
|
||||
SCOPED_SESSION = scoped_session(SESSION)
|
||||
|
||||
model.BASE.query = SCOPED_SESSION.query_property()
|
||||
|
||||
SESSION_HOLDER = local()
|
||||
|
||||
model.BASE.query = SCOPED_SESSION.query_property()
|
||||
@ -99,6 +102,11 @@ def current_session():
|
||||
def create_db():
|
||||
"""Create database."""
|
||||
model.BASE.metadata.create_all(bind=ENGINE)
|
||||
with session() as _s:
|
||||
# Initialize default user
|
||||
user = model.User(email='admin@abc.com', password='admin')
|
||||
logging.info('Init user: %s, %s' % (user.email, user.get_password()))
|
||||
_s.add(user)
|
||||
|
||||
|
||||
def drop_db():
|
||||
|
@ -13,11 +13,12 @@
|
||||
# limitations under the License.
|
||||
|
||||
"""database model."""
|
||||
from datetime import datetime
|
||||
from hashlib import md5
|
||||
import logging
|
||||
import simplejson as json
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, ColumnDefault, Integer, String
|
||||
from sqlalchemy import Float, Enum, DateTime, ForeignKey, Text, Boolean
|
||||
from sqlalchemy import UniqueConstraint
|
||||
@ -27,8 +28,51 @@ from sqlalchemy.ext.hybrid import hybrid_property
|
||||
|
||||
from compass.utils import util
|
||||
|
||||
from flask.ext.login import UserMixin
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
|
||||
BASE = declarative_base()
|
||||
#TODO(grace) SECRET_KEY should be generated when installing compass
|
||||
#and save to a config file or DB
|
||||
SECRET_KEY = "abcd"
|
||||
|
||||
#This is used for generating a token by user's ID and
|
||||
#decode the ID from this token
|
||||
login_serializer = URLSafeTimedSerializer(SECRET_KEY)
|
||||
|
||||
|
||||
class User(BASE, UserMixin):
|
||||
"""User table."""
|
||||
__tablename__ = 'user'
|
||||
id = Column(Integer, primary_key=True)
|
||||
email = Column(String(80), unique=True)
|
||||
password = Column(String(225))
|
||||
active = Column(Boolean, default=True)
|
||||
|
||||
def __init__(self, email, password, **kwargs):
|
||||
self.email = email
|
||||
self.password = self._set_password(password)
|
||||
|
||||
def __repr__(self):
|
||||
return '<User name: %s>' % self.email
|
||||
|
||||
def _set_password(self, password):
|
||||
return self._hash_password(password)
|
||||
|
||||
def get_password(self):
|
||||
return self.password
|
||||
|
||||
def valid_password(self, password):
|
||||
return self.password == self._hash_password(password)
|
||||
|
||||
def get_auth_token(self):
|
||||
return login_serializer.dumps(self.id)
|
||||
|
||||
def is_active(self):
|
||||
return self.active
|
||||
|
||||
def _hash_password(self, password):
|
||||
return md5(password).hexdigest()
|
||||
|
||||
|
||||
class SwitchConfig(BASE):
|
||||
|
9
compass/static/css/bootstrap.min.css
vendored
Normal file
9
compass/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
compass/static/img/glyphicons-halflings-white.png
Normal file
BIN
compass/static/img/glyphicons-halflings-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
BIN
compass/static/img/glyphicons-halflings.png
Normal file
BIN
compass/static/img/glyphicons-halflings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
6
compass/static/js/bootstrap.min.js
vendored
Normal file
6
compass/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
compass/static/js/jquery-1.8.3.min.js
vendored
Normal file
2
compass/static/js/jquery-1.8.3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10
compass/templates/forbidden.jinja
Normal file
10
compass/templates/forbidden.jinja
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends 'layout.jinja' %}
|
||||
|
||||
{% block title %}403 Forbidden{% endblock %}
|
||||
{% block content %}
|
||||
<h1>403 Forbidden</h1>
|
||||
<p class=text-error>
|
||||
Access to this page is forbidden for user
|
||||
<strong>{{ current_user.email }}</strong>.
|
||||
</p>
|
||||
{% endblock %}
|
12
compass/templates/index.jinja
Normal file
12
compass/templates/index.jinja
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends 'layout.jinja' %}
|
||||
|
||||
{% block title %}Main Page{% endblock %}
|
||||
{% block content %}
|
||||
<p>
|
||||
{% if current_user.is_anonymous() %}
|
||||
You are not logged in.
|
||||
{% else %}
|
||||
Hi, <strong>{{ current_user.email }}</strong>!
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endblock %}
|
39
compass/templates/layout.jinja
Normal file
39
compass/templates/layout.jinja
Normal file
@ -0,0 +1,39 @@
|
||||
<!doctype html>
|
||||
|
||||
<meta charset=utf-8>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link rel=stylesheet media=screen href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||
|
||||
<div class=container>
|
||||
<div class=navbar>
|
||||
<div class=navbar-inner>
|
||||
<span class=brand>Flask-Login example</span>
|
||||
<ul class=nav>
|
||||
{% for endpoint in [
|
||||
'index',
|
||||
'restricted',
|
||||
'login' if current_user.is_anonymous() else 'logout',
|
||||
] %}
|
||||
<li {% if endpoint == request.endpoint %}class=active{% endif %}>
|
||||
<a href="{{ url_for(endpoint) }}">{{ endpoint.capitalize() }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert{{ ' alert-%s' % category if category != 'message' else '' }}">
|
||||
<button type=button class=close data-dismiss="alert">×</button>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/jquery-1.8.3.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
|
19
compass/templates/login.jinja
Normal file
19
compass/templates/login.jinja
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends 'layout.jinja' %}
|
||||
{% from 'macros.jinja' import with_errors %}
|
||||
|
||||
{% block title %}Login{% endblock %}
|
||||
{% block content %}
|
||||
<form class=form-horizontal action="{{ url_for('login', next=request.args.get('next')) }}" method=post>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ with_errors(form.email, placeholder='Email') }}
|
||||
{{ with_errors(form.password, placeholder='Password') }}
|
||||
<div class=control-group>
|
||||
<div class=controls>
|
||||
<label class=checkbox>
|
||||
<input type=checkbox name=remember> Remember me
|
||||
</label>
|
||||
<button type=submit class="btn btn-primary">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
13
compass/templates/macros.jinja
Normal file
13
compass/templates/macros.jinja
Normal file
@ -0,0 +1,13 @@
|
||||
{% macro with_errors(field) -%}
|
||||
<div class="control-group{% if field.errors %} error{% endif %}">
|
||||
{{ field.label(class_='control-label') }}
|
||||
<div class=controls>
|
||||
{{ field(**kwargs) }}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
<span class=text-error>{{ error }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
9
compass/templates/restricted.jinja
Normal file
9
compass/templates/restricted.jinja
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends 'layout.jinja' %}
|
||||
|
||||
{% block title %}Restricted Area{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Restricted Area</h1>
|
||||
<p class=text-warning>
|
||||
This is restricted area!
|
||||
</p>
|
||||
{% endblock %}
|
@ -23,7 +23,6 @@ import os
|
||||
import simplejson as json
|
||||
import unittest2
|
||||
|
||||
|
||||
os.environ['COMPASS_IGNORE_SETTING'] = 'true'
|
||||
|
||||
|
||||
@ -32,6 +31,7 @@ reload(setting)
|
||||
|
||||
|
||||
from compass.api import app
|
||||
from compass.api import login_manager
|
||||
from compass.db import database
|
||||
from compass.db.model import Adapter
|
||||
from compass.db.model import Cluster
|
||||
@ -46,6 +46,10 @@ from compass.utils import flags
|
||||
from compass.utils import logsetting
|
||||
from compass.utils import util
|
||||
|
||||
app.config['TESTING'] = True
|
||||
app.config['WTF_CSRF_ENABLED'] = False
|
||||
login_manager.init_app(app)
|
||||
|
||||
|
||||
class ApiTestCase(unittest2.TestCase):
|
||||
"""base api test class."""
|
||||
@ -61,6 +65,7 @@ class ApiTestCase(unittest2.TestCase):
|
||||
logsetting.init()
|
||||
database.init(self.DATABASE_URL)
|
||||
database.create_db()
|
||||
|
||||
self.test_client = app.test_client()
|
||||
|
||||
# We do not want to send a real task as our test environment
|
||||
@ -832,21 +837,15 @@ class ClusterHostAPITest(ApiTestCase):
|
||||
return_value = self.test_client.get(url)
|
||||
self.assertEqual(200, return_value.status_code)
|
||||
config = json.loads(return_value.get_data())['config']
|
||||
|
||||
expected_config = copy.deepcopy(test_config_data)
|
||||
expected_config['hostid'] = 1
|
||||
expected_config['hostname'] = 'host_01'
|
||||
expected_config['clusterid'] = 1
|
||||
expected_config['clustername'] = 'cluster_01'
|
||||
expected_config['fullname'] = 'host_01.1'
|
||||
expected_config[
|
||||
'networking'
|
||||
][
|
||||
'interfaces'
|
||||
][
|
||||
'management'
|
||||
][
|
||||
'mac'
|
||||
] = "00:27:88:0c:01"
|
||||
expected_config['networking']['interfaces']['management'][
|
||||
'mac'] = "00:27:88:0c:01"
|
||||
expected_config['switch_port'] = ''
|
||||
expected_config['switch_ip'] = '192.168.1.1'
|
||||
expected_config['vlan'] = 0
|
||||
@ -856,24 +855,8 @@ class ClusterHostAPITest(ApiTestCase):
|
||||
"""test put clusterhost config."""
|
||||
config = copy.deepcopy(self.test_config_data)
|
||||
config['roles'] = ['base']
|
||||
config[
|
||||
'networking'
|
||||
][
|
||||
'interfaces'
|
||||
][
|
||||
'management'
|
||||
][
|
||||
'ip'
|
||||
] = '192.168.1.2'
|
||||
config[
|
||||
'networking'
|
||||
][
|
||||
'interfaces'
|
||||
][
|
||||
'tenant'
|
||||
][
|
||||
'ip'
|
||||
] = '10.12.1.2'
|
||||
config['networking']['interfaces']['management']['ip'] = '192.168.1.2'
|
||||
config['networking']['interfaces']['tenant']['ip'] = '10.12.1.2'
|
||||
|
||||
# 1. Try to put a config of the cluster host which does not exist
|
||||
url = '/clusterhosts/1000/config'
|
||||
@ -1594,6 +1577,28 @@ class TestExport(ApiTestCase):
|
||||
self.assertDictEqual(export_row, expected_row)
|
||||
self.maxDiff = None
|
||||
|
||||
|
||||
class TestUser(ApiTestCase):
|
||||
def setUp(self):
|
||||
super(TestUser, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super(TestUser, self).tearDown()
|
||||
|
||||
def test_get_user_by_id(self):
|
||||
# Success to get a user
|
||||
url = "/users/1"
|
||||
return_value = self.test_client.get(url)
|
||||
|
||||
data = json.loads(return_value.get_data())
|
||||
self.assertEqual(1, data['user']['id'])
|
||||
|
||||
# Try to get a nonexisting user
|
||||
url = "/users/1000"
|
||||
return_value = self.test_client.get(url)
|
||||
self.assertEqual(404, return_value.status_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
flags.init()
|
||||
logsetting.init()
|
||||
|
126
compass/tests/api/test_auth.py
Normal file
126
compass/tests/api/test_auth.py
Normal file
@ -0,0 +1,126 @@
|
||||
# Copyright 2014 Huawei Technologies Co. Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import simplejson as json
|
||||
import time
|
||||
import unittest2
|
||||
|
||||
os.environ['COMPASS_IGNORE_SETTING'] = 'true'
|
||||
|
||||
|
||||
from compass.utils import setting_wrapper as setting
|
||||
reload(setting)
|
||||
|
||||
from compass.api import app
|
||||
from compass.api import auth
|
||||
from compass.api import login_manager
|
||||
|
||||
from compass.db import database
|
||||
from compass.db.model import User
|
||||
|
||||
from compass.utils import flags
|
||||
from compass.utils import logsetting
|
||||
|
||||
|
||||
login_manager.init_app(app)
|
||||
|
||||
|
||||
class AuthTestCase(unittest2.TestCase):
|
||||
DATABASE_URL = 'sqlite://'
|
||||
USER_CREDENTIALS = {"email": "admin@abc.com", "password": "admin"}
|
||||
|
||||
def setUp(self):
|
||||
super(AuthTestCase, self).setUp()
|
||||
logsetting.init()
|
||||
database.init(self.DATABASE_URL)
|
||||
database.create_db()
|
||||
|
||||
self.test_client = app.test_client()
|
||||
|
||||
def tearDown(self):
|
||||
database.drop_db()
|
||||
super(AuthTestCase, self).tearDown()
|
||||
|
||||
def test_login_logout(self):
|
||||
url = '/login'
|
||||
# a. successfully login
|
||||
data = self.USER_CREDENTIALS
|
||||
return_value = self.test_client.post(url, data=data,
|
||||
follow_redirects=True)
|
||||
|
||||
self.assertIn("Logged in successfully!", return_value.get_data())
|
||||
|
||||
url = '/logout'
|
||||
return_value = self.test_client.get(url, follow_redirects=True)
|
||||
self.assertIn("You have logged out!", return_value.get_data())
|
||||
|
||||
def test_login_failed(self):
|
||||
|
||||
url = '/login'
|
||||
# a. Failed to login with incorrect user info
|
||||
data_list = [{"email": "xxx", "password": "admin"},
|
||||
{"email": "admin@abc.com", "password": "xxx"}]
|
||||
for data in data_list:
|
||||
return_value = self.test_client.post(url, data=data,
|
||||
follow_redirects=True)
|
||||
self.assertIn("Wrong username or password!",
|
||||
return_value.get_data())
|
||||
|
||||
# b. Inactive user
|
||||
User.query.filter_by(email="admin@abc.com").update({"active": False})
|
||||
|
||||
data = {"email": "admin@abc.com", "password": "admin"}
|
||||
return_value = self.test_client.post(url, data=data,
|
||||
follow_redirects=True)
|
||||
self.assertIn("This username is disabled!", return_value.get_data())
|
||||
|
||||
def test_get_token(self):
|
||||
url = '/token'
|
||||
|
||||
# a. Failed to get token by posting incorrect user email
|
||||
req_data = json.dumps({"email": "xxx", "password": "admin"})
|
||||
return_value = self.test_client.post(url, data=req_data)
|
||||
self.assertEqual(401, return_value.status_code)
|
||||
|
||||
# b. Success to get token
|
||||
req_data = json.dumps(self.USER_CREDENTIALS)
|
||||
return_value = self.test_client.post(url, data=req_data)
|
||||
resp = json.loads(return_value.get_data())
|
||||
self.assertIsNotNone(resp['token'])
|
||||
|
||||
def test_header_loader(self):
|
||||
# Get Token
|
||||
url = '/token'
|
||||
req_data = json.dumps(self.USER_CREDENTIALS)
|
||||
return_value = self.test_client.post(url, data=req_data)
|
||||
token = json.loads(return_value.get_data())['token']
|
||||
user_id = auth.get_user_info_from_token(token, 50)
|
||||
self.assertEqual(1, user_id)
|
||||
|
||||
# Test on token expiration.
|
||||
# Sleep 5 seconds but only allow token lifetime of 2 seconds
|
||||
time.sleep(5)
|
||||
user_id = auth.get_user_info_from_token(token, 2)
|
||||
self.assertIsNone(user_id)
|
||||
|
||||
# Get None user from the incorrect token
|
||||
result = auth.get_user_info_from_token("xxx", 50)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
flags.init()
|
||||
logsetting.init()
|
||||
unittest2.main()
|
@ -86,8 +86,8 @@ class ApiTestCase(unittest2.TestCase):
|
||||
# Initial database
|
||||
try:
|
||||
database.init(database_url)
|
||||
except Exception as e:
|
||||
print "======>", e
|
||||
except Exception as error:
|
||||
print "Exception when initializing database: %s" % error
|
||||
|
||||
def tearDown(self):
|
||||
super(ApiTestCase, self).tearDown()
|
||||
@ -126,6 +126,7 @@ class TestAPICommand(ApiTestCase):
|
||||
"""test start deploy from csv."""
|
||||
Client.deploy_hosts = mock.Mock(
|
||||
return_value=(202, self.deploy_return_val))
|
||||
csvdeploy.write_progress_to_file = mock.Mock()
|
||||
url = "http://127.0.0.1:%d" % self.port
|
||||
csvdeploy.start(self.CSV_IMPORT_DIR, url)
|
||||
clusters = csvdeploy.get_csv('cluster.csv',
|
||||
|
@ -2,6 +2,7 @@ flask
|
||||
flask-script
|
||||
flask-restful
|
||||
flask-sqlalchemy
|
||||
flask-login
|
||||
celery
|
||||
netaddr
|
||||
paramiko==1.7.5
|
||||
@ -9,3 +10,5 @@ simplejson
|
||||
requests
|
||||
PyChef
|
||||
redis
|
||||
flask-wtf
|
||||
itsdangerous
|
||||
|
6
setup.py
6
setup.py
@ -76,12 +76,16 @@ setup(
|
||||
long_description='Open Deployment System for zero touch installation',
|
||||
author='Compass Dev Group, Huawei Cloud',
|
||||
author_email='shuo.yang@huawei.com',
|
||||
url='https://github.com/huawei-cloud/compass',
|
||||
url='https://github.com/stackforge/compass-core',
|
||||
download_url='',
|
||||
|
||||
# dependency
|
||||
install_requires=REQUIREMENTS,
|
||||
packages=find_packages(exclude=['compass.tests']),
|
||||
include_package_data=True,
|
||||
#TODO login UI will be replaced by compass's own templates later
|
||||
package_data={'compass': ['templates/*.jinja', 'static/js/*.js',
|
||||
'static/css/*.css', 'static/img/*.png']},
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Console',
|
||||
|
Loading…
Reference in New Issue
Block a user