diff --git a/app/__init__.py b/app/__init__.py index fb7adeb..71cf462 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -256,9 +256,11 @@ def serve_logos(path): ####### from flask_admin.contrib.sqla import ModelView -from app.api.models import Pods, Urls, User, Personalization, Suggestions -from app.utils_db import delete_url_representations, delete_pod_representations, \ - rm_from_npz, add_to_npz, create_pod_in_db, create_pod_npz_pos, rm_doc_from_pos, update_db_idvs_after_npz_delete +from app.api.models import AccessLogs, Pods, Urls, User, Personalization, Suggestions +from app.utils_db import ( + create_access_log_entry, compute_daily_access_stats, delete_url_representations, delete_pod_representations, rm_from_npz, add_to_npz, create_pod_in_db, + create_pod_npz_pos, rm_doc_from_pos, update_db_idvs_after_npz_delete +) from flask_admin import expose from flask_admin.contrib.sqla.view import ModelView @@ -266,8 +268,10 @@ def serve_logos(path): # Authentification class MyLoginManager(LoginManager): + unauthorized_status_code = 404 + def unauthorized(self): - return abort(404) + return abort(self.unauthorized_status_code) login_manager = MyLoginManager() login_manager.login_view = 'auth.login' @@ -281,17 +285,80 @@ def load_user(user_id): return User.query.get(int(user_id)) +# Admin access logs +@app.after_request +def log_endpoint_accessed(response): + if "/admin/" in request.url: + return response + + user_logged_in = user_is_confirmed = user_is_admin = False + user_email = None + user_id = -1 + if current_user.is_authenticated: + user_id = current_user.id + user_logged_in = True + user_email = current_user.email + user_is_confirmed = current_user.is_confirmed + user_is_admin = current_user.is_admin + + if request.endpoint is None: + endpoint = "" + else: + endpoint = request.endpoint + if endpoint in ["static", "serve_sw"]: + event_type = "load_resource" + elif endpoint.startswith("api."): + event_type = "api_request" + elif request.method == "POST": + event_type = "form_submit" + else: + event_type = "view_page" + + create_access_log_entry( + user_logged_in, + user_id, + user_is_confirmed, + user_is_admin, + user_email, + event_type, + request.endpoint, + request.url, + response.status_code, + None + ) + return response + # Flask and Flask-SQLAlchemy initialization here +from app.auth.decorators import log_auth_failure def can_access_flaskadmin(): + # replaces what the authentication decorators do, for modelview endpoints if not current_user.is_authenticated: + log_auth_failure(404, "unauthenticated user tried to access admin modelview endpoint") return abort(404) if not current_user.is_admin: + "non-admin user tried to access admin modelview endpoint" return abort(404) return True class MyAdminIndexView(AdminIndexView): + + @expose() + def index(self): + def _format_stats(stats): + fmt_stats = {} + for stat_name, stat_val in stats.items(): + if "_diff" in stat_name: + fmt_stats[stat_name] = f"{'+' if stat_val > 0 else ''}{stat_val}" + else: + fmt_stats[stat_name] = str(stat_val) + return fmt_stats + + access_stats = compute_daily_access_stats() + + return self.render(self._template, access_stats=_format_stats(access_stats)) + def is_accessible(self): return can_access_flaskadmin() @@ -475,11 +542,20 @@ class SuggestionsModelView(ModelView): def is_accessible(self): return can_access_flaskadmin() +class AccessLogsModelView(ModelView): + list_template = 'admin/pears_list.html' + column_searchable_list = ['user_id', 'event_type', 'endpoint'] + can_edit = False + page_size = 50 + def is_accessible(self): + return can_access_flaskadmin() + admin.add_view(PodsModelView(Pods, db.session)) admin.add_view(UrlsModelView(Urls, db.session)) admin.add_view(UsersModelView(User, db.session)) admin.add_view(PersonalizationModelView(Personalization, db.session)) admin.add_view(SuggestionsModelView(Suggestions, db.session)) +admin.add_view(AccessLogsModelView(AccessLogs, db.session)) @app.errorhandler(404) def page_not_found(e): diff --git a/app/api/models.py b/app/api/models.py index b7e91f7..5f7e35e 100644 --- a/app/api/models.py +++ b/app/api/models.py @@ -41,6 +41,60 @@ class Base(db.Model): onupdate=db.func.current_timestamp()) +class AccessLogs(db.Model): + id = db.Column(db.Integer, primary_key=True) + log_date = db.Column(db.DateTime, default=db.func.current_timestamp()) + user_logged_in = db.Column(db.Boolean) + user_id = db.Column(db.Integer) + user_email = db.Column(db.String(1000)) + user_is_confirmed = db.Column(db.Boolean) + user_is_admin = db.Column(db.Boolean) + event_type = db.Column(db.String(1000)) + endpoint = db.Column(db.String(1000)) + requested_url = db.Column(db.String(1000)) + response_code = db.Column(db.Integer) + messages = db.Column(db.String(1000)) + + def __init__( + self, + logged_in, + user_id, + user_is_confirmed, + user_is_admin, + user_email, + event_type, + endpoint, + requested_url, + response_code, + messages + ): + self.user_logged_in = logged_in + self.user_id = user_id + self.user_email = user_email + self.user_is_confirmed = user_is_confirmed + self.user_is_admin = user_is_admin + self.event_type = event_type + self.endpoint = endpoint + self.requested_url = requested_url + self.response_code = response_code + self.messages = messages + + @property + def serialize(self): + return { + "id": self.id, + "log_date": self.log_date, + "logged_in": self.logged_in, + "user_id": self.user_id, + "user_email": self.user_email, + "event_type": self.event_type, + "endpoint": self.endpoint, + "requested_url": self.requested_url, + "response_code": self.response_code, + "messages": self.messages + } + + class Suggestions(Base): id = db.Column(db.Integer, primary_key=True) url = db.Column(db.String(1000)) diff --git a/app/auth/controllers.py b/app/auth/controllers.py index 059407e..de3fcfe 100644 --- a/app/auth/controllers.py +++ b/app/auth/controllers.py @@ -16,6 +16,7 @@ from app.auth.decorators import check_permissions from app.auth.token import send_email, send_reset_password_email, generate_token, confirm_token from app.auth.captcha import mk_captcha, check_captcha +from app.utils_db import create_access_log_entry # Define the blueprint: auth = Blueprint('auth', __name__, url_prefix='/auth') @@ -25,7 +26,23 @@ @auth.route('/logout') @check_permissions(login=True) def logout(): + user_id = current_user.id + user_is_confirmed = current_user.is_confirmed + user_is_admin = current_user.is_admin + user_email = current_user.email logout_user() + create_access_log_entry( + True, + user_id, + user_is_confirmed, + user_is_admin, + user_email, + "auth_success", + request.endpoint, + request.url, + None, + "successfully logged out user" + ) flash(gettext("You have successfully logged out."), "success") return redirect(url_for("search.index")) @@ -48,9 +65,33 @@ def login(): # take the user-supplied password, hash it, and compare it to the hashed password in the database try: if not user or not check_password_hash(user.password, password): + create_access_log_entry( + False, + user.id if user else -1, + user.is_confirmed if user else False, + user.is_admin if user else False, + user.email if user else None, + "auth_failure", + request.endpoint, + request.url, + None, + "incorrect details entered when logging in" + ) flash(gettext('Please check your login details and try again.')) return redirect(url_for('auth.login')) # if the user doesn't exist or password is wrong, reload the page except: #the check_password_hash method has failed + create_access_log_entry( + False, + user.id if user else -1, + user.is_confirmed if user else False, + user.is_admin if user else False, + user.email if user else None, + "auth_failure", + request.endpoint, + request.url, + None, + "checking password hash failed" + ) flash(gettext("We have moved to a more secure authentification method. Please request a password change.")) return redirect(url_for('auth.password_forgotten')) @@ -63,6 +104,18 @@ def login(): flash(msg) else: welcome = ""+gettext('Welcome')+", "+current_user.username+"!" + create_access_log_entry( + False, + user.id, + user.is_confirmed, + user.is_admin, + user.email, + "auth_success", + request.endpoint, + request.url, + None, + "user logged in" + ) return redirect(url_for("search.index")) print(form.errors) return render_template('auth/login.html', form=form, new_users_allowed=new_users_allowed) @@ -89,14 +142,51 @@ def signup(): if user1 : # if a user is found, we want to redirect back to signup page so user can try again flash(gettext('Email address already exists.')) + + create_access_log_entry( + False, + user1.id, + user1.is_confirmed, + user1.is_admin, + user1.email, + "auth_failure", + request.endpoint, + request.url, + None, + "new user attempted to sign up with existing email address" + ) return redirect(url_for('auth.signup')) if user2 : # if a user is found, we want to redirect back to signup page so user can try again flash(gettext('Username already exists.')) + create_access_log_entry( + False, + user2.id, + user2.is_confirmed, + user2.is_admin, + user2.email, + "auth_failure", + request.endpoint, + request.url, + None, + f"new user attempted to sign up with existing username. (N.B.: logged username is the existing user's. new user's email: {request.form.get('email')})" + ) return redirect(url_for('auth.signup')) if not check_captcha(captcha, captcha_answer): flash(gettext('The captcha was incorrectly answered.')) + create_access_log_entry( + False, + -1, + False, + False, + email, + "auth_failure", + request.endpoint, + request.url, + None, + "new user attempted to sign up but failed the captcha check" + ) return redirect(url_for('auth.signup')) print("Signup form correctly validated.") @@ -123,11 +213,40 @@ def signup(): db.session.commit() login_user(new_user) - + create_access_log_entry( + False, + new_user.id, + new_user.is_confirmed, + new_user.is_admin, + new_user.email, + "auth_success", + request.endpoint, + request.url, + None, + "new user successfully signed up and logged in" + ) flash(gettext("Welcome! Your signup is almost complete; confirm your email address to fully activate your account."), "success") return redirect(url_for("auth.inactive")) - else: + elif request.method == "POST": print("FORM ERRORS:", form.errors) + captcha = mk_captcha() + form = RegistrationForm(request.form) + form.captcha.data = captcha + form.captcha_answer.label = captcha + create_access_log_entry( + False, + -1, + False, + False, + request.form.get("email"), + "auth_failure", + request.endpoint, + request.url, + None, + "new user tried to sign up but form did not validate" + ) + return render_template('auth/signup.html', form=form, new_users_allowed=new_users_allowed) + else: captcha = mk_captcha() form = RegistrationForm(request.form) form.captcha.data = captcha @@ -138,6 +257,18 @@ def signup(): @check_permissions(login=True) def confirm_email(token): if current_user.is_confirmed: + create_access_log_entry( + False, + current_user.id, + current_user.is_confirmed, + current_user.is_admin, + current_user.email, + "auth_failure", + request.endpoint, + request.url, + None, + "already confirmed user attempted to confirm from link in email" + ) flash(gettext("Account already confirmed."), "success") return redirect(url_for("search.index")) email = confirm_token(token) @@ -147,8 +278,32 @@ def confirm_email(token): user.confirmed_on = datetime.now() db.session.add(user) db.session.commit() + create_access_log_entry( + True, + current_user.id, + current_user.is_confirmed, + current_user.is_admin, + current_user.email, + "auth_success", + request.endpoint, + request.url, + None, + "successfully confirmed user from link in email" + ) flash(gettext("You have confirmed your account. Thanks!"), "success") else: + create_access_log_entry( + current_user.is_authenticated, + current_user.id, + current_user.is_confirmed, + current_user.is_admin, + current_user.email, + "auth_failure", + request.endpoint, + request.url, + None, + "unconfirmed user attempted to confirm using invalid link" + ) flash(gettext("The confirmation link is invalid or has expired."), "danger") return redirect(url_for("search.index")) @@ -156,6 +311,18 @@ def confirm_email(token): @check_permissions(login=True) def resend_confirmation(): if current_user.is_confirmed: + create_access_log_entry( + True, + current_user.id, + current_user.is_confirmed, + current_user.is_admin, + current_user.email, + "auth_failure", + request.endpoint, + request.url, + None, + "already confirmed user attempted to obtain new confirmation link" + ) flash(gettext("Your account has already been confirmed."), "success") return redirect(url_for("search.index")) token = generate_token(current_user.email) @@ -164,6 +331,18 @@ def resend_confirmation(): subject = gettext("Please confirm your email.") send_email(current_user.email, subject, html) flash(gettext("A new confirmation email has been sent."), "success") + create_access_log_entry( + True, + current_user.id, + current_user.is_confirmed, + current_user.is_admin, + current_user.email, + "auth_success", + request.endpoint, + request.url, + None, + "successfully resent authentication link" + ) return redirect(url_for("auth.inactive")) @@ -185,15 +364,50 @@ def password_forgotten(): html = render_template("auth/password_reset_email.html", confirm_url=confirm_url) subject = gettext("You have requested a password reset.") send_reset_password_email(user.email, subject, html) - + create_access_log_entry( + False, + user.id if user else -1, + user.is_confirmed if user else False, + user.is_admin if user else False, + request.form.get("email"), + "auth_success" if user else "auth_failure", + request.endpoint, + request.url, + None, + "user successfully requested password reset" if user else "non-existent user attempts password reset" + ) flash(gettext("A link has been sent via email to reset your password."), "success") return redirect(url_for('auth.login')) else: + create_access_log_entry( + False, + -1, + False, + False, + request.form.get("email"), + "auth_failure", + request.endpoint, + request.url, + None, + "user attempted to request password reset but form did not validate" + ) return render_template('auth/password_forgotten.html', form=form) @auth.route("/password-reset-confirm/") def password_reset(token): if current_user.is_authenticated: + create_access_log_entry( + True, + current_user.id, + current_user.is_confirmed, + current_user.is_admin, + current_user.email, + "auth_failure", + request.endpoint, + request.url, + None, + "already logged-in user attempted to confirm password reset request" + ) return redirect(url_for('search.index')) form = PasswordChangeForm(request.form) email = confirm_token(token) @@ -201,11 +415,47 @@ def password_reset(token): user = User.query.filter_by(email=email).first() if user.email == email: login_user(user) + create_access_log_entry( + False, + user.id, + user.is_confirmed, + user.is_admin, + request.form.get("email"), + "auth_success", + request.endpoint, + request.url, + None, + "user successfully confirmed password reset and was logged in" + ) return render_template('auth/password_change.html', username=user.username, form=form) else: + create_access_log_entry( + False, + user.id if user else -1, + user.is_confirmed if user else False, + user.is_admin if user else False, + request.form.get("email"), + "auth_failure", + request.endpoint, + request.url, + None, + "user attempted to confirm password reset with invalid confirmation link" + ) flash(gettext("The confirmation link is invalid or has expired."), "danger") return redirect(url_for("auth.password_forgotten")) else: + create_access_log_entry( + False, + -1, + False, + False, + request.form.get("email"), + "auth_failure", + request.endpoint, + request.url, + None, + "user attempted to confirm password reset with invalid confirmation link" + ) flash(gettext("The confirmation link is invalid or has expired."), "danger") return redirect(url_for("auth.password_forgotten")) @@ -220,13 +470,36 @@ def password_change(): password = request.form.get('password') user.password=generate_password_hash(password, method='scrypt') db.session.commit() + create_access_log_entry( + False, + user.id, + user.is_confirmed, + user.is_admin, + user.email, + "auth_success", + request.endpoint, + request.url, + None, + "user successfully changed password" + ) flash(gettext("Your password has been successfully changed."), "success") return redirect(url_for("search.index")) else: + create_access_log_entry( + True, + current_user.id, + current_user.is_confirmed, + current_user.is_admin, + current_user.email, + "auth_failure", + request.endpoint, + request.url, + None, + "user attempted to change password but form did not validate" + ) return render_template('auth/password_change.html', form=form) - ''' INACTIVE ''' @auth.route("/inactive") diff --git a/app/auth/decorators.py b/app/auth/decorators.py index a354318..3657dd8 100644 --- a/app/auth/decorators.py +++ b/app/auth/decorators.py @@ -1,13 +1,29 @@ from flask_login import current_user from functools import wraps -from flask import render_template, url_for, flash, current_app +from flask import render_template, url_for, flash, current_app, request from flask_babel import gettext +from app.utils_db import create_access_log_entry def get_func_identifier(func): return func.__module__ + "." + func.__name__ +def log_auth_failure(error_code, error_msg): + create_access_log_entry( + current_user.is_authenticated, + current_user.id if current_user.is_authenticated else -1, + current_user.is_confirmed if current_user.is_authenticated else False, + current_user.is_admin if current_user.is_authenticated else False, + current_user.email if current_user.is_authenticated else None, + "auth_failure", + request.endpoint, + request.url, + error_code, + error_msg + ) + + def check_permissions(login=False, confirmed=False, admin=False): def decorator(func): @wraps(func) @@ -40,6 +56,10 @@ def check_is_logged_in(func): @wraps(func) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: + log_auth_failure( + current_app.login_manager.unauthorized_status_code, + "unauthenticated user tried to access login-only endpoint" + ) return current_app.login_manager.unauthorized() return func(*args, **kwargs) return decorated_function @@ -48,9 +68,17 @@ def check_is_confirmed(func): @wraps(func) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: + log_auth_failure( + current_app.login_manager.unauthorized_status_code, + "unauthenticated user tried to access confirmed-only endpoint" + ) return current_app.login_manager.unauthorized() if current_user.is_confirmed is False: flash(gettext("You are trying to access a page that requires your account to be verified."), "warning") + log_auth_failure( + 403, + "unconfirmed user tried to access confirmed-only endpoint" + ) return render_template("auth/inactive.html"), 403 return func(*args, **kwargs) return decorated_function @@ -59,8 +87,16 @@ def check_is_admin(func): @wraps(func) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: + log_auth_failure( + current_app.login_manager.unauthorized_status_code, + "unauthenticated user tried to access admin-only endpoint" + ) return current_app.login_manager.unauthorized() if current_user.is_admin is False: + log_auth_failure( + current_app.login_manager.unauthorized_status_code, + "user without admin rights tried to access admin-only endpoint" + ) return current_app.login_manager.unauthorized() return func(*args, **kwargs) diff --git a/app/templates/admin/index.html b/app/templates/admin/index.html index 284b9a8..5d8a49f 100644 --- a/app/templates/admin/index.html +++ b/app/templates/admin/index.html @@ -17,8 +17,34 @@ {{gettext('Users')}} {{gettext('Suggestions')}} {{gettext('Personalization')}} + {{gettext('Access Logs')}} + +
+

Daily access summary

+
+ All numbers shown are for today ({{access_stats.date}}). + Numbers shown between parentheses show the change of today's numbers for each statistic + compared to yesterday. + For more details, check out the Access Logs. +
+
+
    +
  • {{gettext('Total number of requests:')}} {{access_stats.total_requests}} ({{access_stats.total_requests_diff}})
  • +
      +
    • {{gettext('Of which successful:')}} {{access_stats.successful_requests}} ({{access_stats.successful_requests_diff}})
    • +
    • {{gettext('Of which failed:')}} {{access_stats.failed_requests}} ({{access_stats.failed_requests_diff}})
    • +
    +
  • {{gettext('Total number of log-in attempts:')}} {{access_stats.total_logins}} ({{access_stats.total_logins_diff}})
  • +
      +
    • {{gettext('Of which successful:')}} {{access_stats.successful_logins}} ({{access_stats.successful_logins_diff}})
    • +
    • {{gettext('Of which failed:')}} {{access_stats.failed_logins}} ({{access_stats.failed_logins_diff}})
    • +
    +
  • {{gettext('Unique users:')}} {{access_stats.unique_users}} ({{access_stats.unique_users_diff}})
  • +
+
+ {% endblock %} diff --git a/app/utils_db.py b/app/utils_db.py index 2ef1b8a..7067ddd 100644 --- a/app/utils_db.py +++ b/app/utils_db.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only import logging +import datetime from os import remove, rename, getenv from os.path import dirname, realpath, join, isfile from pathlib import Path @@ -12,7 +13,7 @@ import numpy as np from scipy.sparse import csr_matrix, load_npz, vstack, save_npz from app import db, models, VEC_SIZE -from app.api.models import Urls, Pods, Suggestions +from app.api.models import AccessLogs, Urls, Pods, Suggestions from app.indexer.posix import load_posix, dump_posix dir_path = dirname(dirname(realpath(__file__))) @@ -26,6 +27,146 @@ def parse_pod_name(pod_name): return contributor, theme, lang +########### +# Access logging +########### + +def create_access_log_entry( + user_logged_in, + user_id, + user_is_confirmed, + user_is_admin, + user_email, + event_type, + endpoint, + request_url, + response_code, + messages +): + log = AccessLogs( + user_logged_in, + user_id, + user_is_confirmed, + user_is_admin, + user_email, + event_type, + endpoint, + request_url, + response_code, + messages + ) + db.session.add(log) + db.session.commit() + + +def compute_daily_access_stats(): + today = datetime.date.today() + yesterday = today - datetime.timedelta(days=1) + before_yesterday = today - datetime.timedelta(days=2) + + successful_access_logs_today = ( + db.session.query(AccessLogs) + .filter(AccessLogs.log_date > yesterday) + .filter(AccessLogs.response_code.between(199,400)) + .all() + ) + successful_access_logs_yesterday = ( + db.session.query(AccessLogs) + .filter(AccessLogs.log_date + .between(before_yesterday, yesterday)) + .filter(AccessLogs.response_code.between(199,400)) + .all() + ) + + failed_access_logs_today = ( + db.session.query(AccessLogs) + .filter(AccessLogs.log_date > yesterday) + .filter(AccessLogs.response_code.between(399, 600)) + .all() + ) + + failed_access_logs_yesterday = ( + db.session.query(AccessLogs) + .filter(AccessLogs.log_date.between(before_yesterday, yesterday)) + .filter(AccessLogs.response_code.between(399, 600)) + .all() + ) + + total_requests_today = len(successful_access_logs_today) + len(failed_access_logs_today) + total_requests_yesterday = len(successful_access_logs_yesterday) + len(failed_access_logs_yesterday) + + successful_logins_today = ( + db.session.query(AccessLogs) + .filter(AccessLogs.log_date > yesterday) + .filter(AccessLogs.endpoint == "auth.login") + .filter(AccessLogs.event_type == "auth_success") + .all() + ) + + successful_logins_yesterday = ( + db.session.query(AccessLogs) + .filter(AccessLogs.log_date.between(before_yesterday, yesterday)) + .filter(AccessLogs.endpoint == "auth.login") + .filter(AccessLogs.event_type == "auth_success") + .all() + ) + + failed_logins_today = ( + db.session.query(AccessLogs) + .filter(AccessLogs.log_date > yesterday) + .filter(AccessLogs.endpoint == "auth.login") + .filter(AccessLogs.event_type == "auth_failure") + .all() + ) + + failed_logins_yesterday = ( + db.session.query(AccessLogs) + .filter(AccessLogs.log_date.between(before_yesterday, yesterday)) + .filter(AccessLogs.endpoint == "auth.login") + .filter(AccessLogs.event_type == "auth_failure") + .all() + ) + + total_logins_today = len(successful_logins_today) + len(failed_logins_today) + total_logins_yesterday = len(successful_logins_yesterday) + len(failed_logins_yesterday) + + unique_users_today = ( + db.session.query(AccessLogs) + .filter(AccessLogs.log_date > yesterday) + .filter(AccessLogs.user_id != -1) + .group_by(AccessLogs.user_id) + .count() + ) + + unique_users_yesterday = ( + db.session.query(AccessLogs) + .filter(AccessLogs.log_date.between(before_yesterday, yesterday)) + .filter(AccessLogs.user_id != -1) + .group_by(AccessLogs.user_id) + .count() + ) + + return { + "date": f"{today.year}-{today.month}-{today.day}", + "total_requests": total_requests_today, + "total_requests_diff": total_requests_yesterday - total_requests_today, + "successful_requests": len(successful_access_logs_today), + "successful_requests_diff": len(successful_access_logs_today) - len(successful_access_logs_yesterday), + "failed_requests": len(failed_access_logs_today), + "failed_requests_diff": len(failed_access_logs_today) - len(failed_access_logs_yesterday), + + "total_logins": total_logins_today, + "total_logins_diff": total_logins_today - total_logins_yesterday, + "successful_logins": len(successful_logins_today), + "successful_logins_diff": len(successful_logins_today) - len(successful_logins_yesterday), + "failed_logins": len(failed_logins_today), + "failed_logins_diff": len(failed_logins_today) - len(failed_logins_yesterday), + + "unique_users": unique_users_today, + "unique_users_diff": unique_users_today - unique_users_yesterday + } + + ########### # Creating ###########