Welcome to SAM API’s Documentation

SAM

Welcome to SAM API’s documentation. This document is aimed at developers and it is highly recommended to start with the Installation section and then go forward with the API sections. The main purpose of this documentation is to provide insights into how one should use SAM’s core API services in the development of a frontend (e.g., the default one provided at https://github.com/SECURIoTESIGN/SAM).

Project Overview

Security Advising Modules (SAM) is a framework that aggregates and connects all the various modules developed under the scope of the SECURIoTESIGN project while providing a graphical interface to interact with them. SAM is modular, allowing for both quick integrations of newly developed modules and updates to existing modules. SAM works as a personal security assistant, more relatable, and capable of communicating with the end-user through a series of easily understandable questions, which the user can answer directly or through a set of predefined answers.

Installation

The installation, for development proposes only, can be accomplished through a group of Docker containers.

  1. Clone the public repository.

  2. Create the API and database docker imagens and subsequent containers:

cd sam-api
make

Note

The database and API container will be deployed with API port set to 8080 and database port set to 3306. The database user will be root and the password secure.

  1. Deploy the database into the database container:

sudo docker exec -it sam-db python3 install_db.py

You can access the database using DBeaver or similar (use sudo docker inspect sam-db to get the IP of the sam-db container).

  1. Develop away!

You can start both containers as follows:

sudo docker stop sam-api && sudo docker start sam-api

SAM’s REST services can be tested and accessed through insomnia – The Insomnia JSON file of this project can be found on the following link.

Important

The contents of folder sam-api are mapped to the sam-api container, any local changes will be reflected.

Core and Community Modules/Plugins

Several module and plugins have been developed by the SECURIoTESIGN team and the community. Namely:

  • Security Requirements Elicitation (SRE) - This module will elicit the main security requirements and properties, e.g., confidentiality, integrity, authentication, access control, availability or accountability that the system should implement from the basic information of what composes it and defines it (e.g., type of system, methods of authentication, types of users, sensitive information storage).

  • Security Best Best Practices (SBPG) - This module will provide best practices guildelines that will highlight common potential vulnerabilities that need to be taken into account during development, and provide information on secure practices that should be implemented.

  • Lightweight Criptographic Algorithms Recommendation (LWCAR) - This module will take the information on the system hardware requirements and defined security requirements. It also proposes algorithms that can be implemented to ensure the security requirements are fulfilled. It specifically recommends lightweight cryptographic algorithms, for both software and hardware implementations.

  • Threat Modeling Solution (TMS) - This modules provides a set of threats that based on the answers given by a user.

  • Assessment of the Correct Integration of Security Mechanisms (ACISM) - This module outputs system tests to check for the presence of threats.

These modules can be found at https://github.com/SECURIoTESIGN/SAM-Modules.

Important

A module is defined as a component that incorporates questions and answers and an output designated as recommendations, while a plugin is a special module similar to the latter but it only provides the output (i.e., recommendations) – These plugins are dependent on the input of other modules.

Installing Core Modules

To install core modules into SAM:

cd sam-api
make modules

Important

Beware, this process will recreate SAM’s database - All data will be lost!

Developing Modules

The development of modules can be accomplished by using the services detailed in section Modules Services API. If there is a need to develop modules with some logic – that is, without being dependent on the mapping between question, answer, and recommendation provided by core services – an example is available on the following link.

Note

If a logic file is developed for a module those services detailed in section Add module should be used in conjuntion with the file API detailed in Upload File.

Statistics Services API

This section includes details concerning services developed for the statistic entity implemented in
statistic.py.
  1"""
  2// ---------------------------------------------------------------------------
  3//
  4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
  5//
  6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
  7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
  8//
  9//  This program is free software: you can redistribute it and/or modify
 10//  it under the terms of the GNU General Public License as published by
 11//  the Free Software Foundation, either version 3 of the License, or
 12//  (at your option) any later version.
 13//
 14//  This program is distributed in the hope that it will be useful,
 15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
 16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17//  GNU General Public License for more details.
 18//
 19//  You should have received a copy of the GNU General Public License
 20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 21// 
 22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
 23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
 24//  POCI-01-0145-FEDER-030657) 
 25// ---------------------------------------------------------------------------
 26"""
 27from api import app, mysql
 28from flask import request
 29import views.user, modules.utils, views.recommendation, views.module, views.session
 30
 31
 32"""
 33[Summary]: Get Global Stats
 34[Returns]: Response result.
 35"""
 36@app.route("/api/statistics", methods=['GET'])
 37def get_global_stats():
 38    if request.method != 'GET': return
 39    
 40    views.user.isAuthenticated(request)
 41
 42    stats = {}
 43    tmp_users = get_number_of_table('user')
 44    tmp_modules = get_number_of_table('module')
 45    tmp_questions = get_number_of_table('question')
 46    tmp_answers = get_number_of_table('answer')
 47    tmp_sessions = get_number_of_table('session')
 48    tmp_recommendations = get_number_of_table('recommendation')
 49
 50    stats['users']              = tmp_users or 0
 51    stats['modules']            = tmp_modules or 0
 52    stats['questions']          = tmp_questions or 0
 53    stats['answers']            = tmp_answers or 0
 54    stats['sessions']           = tmp_sessions or 0
 55    stats['recommendations']    = tmp_recommendations or 0
 56
 57    # 'May the Force be with you.'
 58    return(modules.utils.build_response_json(request.path, 200, stats)) 
 59
 60"""
 61[Summary]: Get the number of users 
 62[Returns]: Response result.
 63"""
 64@app.route("/api/statistic/users", methods=['GET'])
 65def get_stats_user(internal_call=False):
 66    if (not internal_call): 
 67        if request.method != 'GET': return
 68    
 69    # Check if the user has permissions to access this resource
 70    if (not internal_call):
 71        views.user.isAuthenticated(request)
 72    
 73    # Let's get the info from the databse.
 74    try:
 75        conn    = mysql.connect()
 76        cursor  = conn.cursor()
 77        cursor.execute("SELECT COUNT(id) as size FROM user")
 78        res = cursor.fetchall()
 79    except Exception as e:
 80        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 81    
 82    # Check for empty results 
 83    if (len(res) == 0):
 84        cursor.close()
 85        conn.close()
 86        if (not internal_call):
 87            return(modules.utils.build_response_json(request.path, 404))
 88        else:
 89            return(None)
 90    else:
 91        data = {}
 92        for row in res:
 93            data['size'] = row[0]
 94            break
 95    
 96    cursor.close()
 97    conn.close()
 98
 99    # 'May the Force be with you, young master'.
100    if (not internal_call):
101        return(modules.utils.build_response_json(request.path, 200, data)) 
102    else:
103        return(data)
104"""
105[Summary]: Get the number of modules 
106[Returns]: Response result.
107"""
108@app.route("/api/statistic/modules", methods=['GET'])
109def get_stats_modules(internal_call=False):
110    if (not internal_call): 
111        if request.method != 'GET': return
112    
113    # Check if the user has permissions to access this resource
114    if (not internal_call):
115        views.user.isAuthenticated(request)
116    
117    # Let's get the info from the databse.
118    try:
119        conn    = mysql.connect()
120        cursor  = conn.cursor()
121        # Top 5 only
122        cursor.execute("SELECT shortname, displayname, occurrences FROM module, (SELECT moduleID as mID, count(*) as occurrences FROM session GROUP BY moduleID ORDER BY occurrences DESC LIMIT 5) as Top5 WHERE ID = mID")
123        res = cursor.fetchall()
124    except Exception as e:
125        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
126    
127    # Check for empty results 
128    if (len(res) == 0):
129        cursor.close()
130        conn.close()
131        if (not internal_call):
132            return(modules.utils.build_response_json(request.path, 200, {"size":0}))
133        else:
134            return(None)
135    else:
136        tot_mod = 0
137        stat = {}
138        stat['top'] = []
139        for row in res:
140            module = {}
141            module['shortname'] = row[0]
142            module['displayname'] = row[1]
143            module['occurrences'] = row[2]
144            tot_mod += row[2]
145            stat['top'].append(module)
146        
147        stat['size'] = tot_mod
148
149    cursor.close() 
150    conn.close()
151
152    # 'May the Force be with you, young master'.
153    if (not internal_call):
154        return(modules.utils.build_response_json(request.path, 200, stat))
155    else: 
156        print("--->" + stat)
157        return(stat)
158
159"""
160[Summary]: Get the number of questions 
161[Returns]: Response result.
162"""
163@app.route("/api/statistic/questions", methods=['GET'])
164def get_stats_questions(internal_call=False):
165    if (not internal_call):
166        if request.method != 'GET': return
167
168    # Check if the user has permissions to access this resource
169    if (not internal_call):
170        views.user.isAuthenticated(request)
171    
172    # Let's get the info from the databse.
173    try:
174        conn    = mysql.connect()
175        cursor  = conn.cursor()
176        cursor.execute("SELECT COUNT(id) as size FROM question")
177        res = cursor.fetchall()
178    except Exception as e:
179        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
180    
181    # Check for empty results 
182    if (len(res) == 0):
183        cursor.close()
184        conn.close()
185        if (not internal_call):
186            return(modules.utils.build_response_json(request.path, 404))
187        else:
188            return(None) 
189    else:
190        data = {}
191        for row in res:
192            data['size'] = row[0]
193            break
194    
195    cursor.close()
196    conn.close()
197
198    # 'May the Force be with you, young master'.
199    if (not internal_call):
200        return(modules.utils.build_response_json(request.path, 200, data))
201    else:
202        return(data)
203
204"""
205[Summary]: Get the number of answers 
206[Returns]: Response result.
207"""
208@app.route("/api/statistic/answers", methods=['GET'])
209def get_stats_answers(internal_call=False):
210    if (not internal_call):
211        if request.method != 'GET': return
212    # Check if the user has permissions to access this resource
213    if (not internal_call):
214        views.user.isAuthenticated(request)
215    
216    # Let's get the info from the databse.
217    try:
218        conn    = mysql.connect()
219        cursor  = conn.cursor()
220        cursor.execute("SELECT COUNT(id) as size FROM answer")
221        res = cursor.fetchall()
222    except Exception as e:
223        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
224    
225    # Check for empty results 
226    if (len(res) == 0):
227        cursor.close()
228        conn.close()
229        if (not internal_call):
230            return(modules.utils.build_response_json(request.path, 404))
231        else: 
232            return(None)
233    else:
234        data = {}
235        for row in res:
236            data['size'] = row[0]
237            break
238    
239    cursor.close()
240    conn.close()
241    # 'May the Force be with you, young master'.
242    if (not internal_call):
243        return(modules.utils.build_response_json(request.path, 200, data))
244    else:
245        return(data)
246
247"""
248[Summary]: Get the number of recommendations 
249[Returns]: Response result.
250"""
251@app.route("/api/statistic/recommendations", methods=['GET'])
252def get_stats_recommendations(internal_call=False):
253    if (not internal_call):
254        if request.method != 'GET': return
255    # Check if the user has permissions to access this resource
256    if (not internal_call):
257        views.user.isAuthenticated(request)
258    
259    # Let's get the info from the databse.
260    try:
261        conn    = mysql.connect()
262        cursor  = conn.cursor()
263        # Top 5 only
264        cursor.execute("SELECT content, occurrences FROM recommendation, (SELECT recommendationID as rID, count(*) as occurrences FROM session_recommendation GROUP BY recommendationID ORDER BY occurrences DESC LIMIT 5) as Top5 WHERE ID = rID;")
265        res = cursor.fetchall()
266    except Exception as e:
267        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
268    
269    # Check for empty results 
270    if (len(res) == 0):
271        cursor.close()
272        conn.close()
273        if (not internal_call):
274            return(modules.utils.build_response_json(request.path, 200, {"size":0}))
275        else:
276            return(None)
277    else:
278        tot_recm = 0
279        stat = {}
280        stat['top'] = []
281        for row in res:
282            recommendation = {}
283            recommendation['occurrences'] = row[1]
284            recommendation['content'] = row[0]
285            tot_recm += row[1]
286            stat['top'].append(recommendation)
287        
288        stat['size'] = tot_recm
289
290    cursor.close()
291    conn.close()
292    # 'May the Force be with you, young master'.
293    if (not internal_call):
294        return(modules.utils.build_response_json(request.path, 200, stat))
295    else:
296        return(stat)
297
298
299"""
300[Summary]: Get the number of sessions in the last 7 days.
301[Returns]: Response result.
302"""
303@app.route("/api/statistic/sessions", methods=['GET'])
304def get_stats_sessions(internal_call=False):
305    if (not internal_call):
306        if request.method != 'GET': return
307    # Check if the user has permissions to access this resource
308    if (not internal_call):
309        views.user.isAuthenticated(request)
310    
311    # Let's get the info from the databse.
312    try:
313        conn    = mysql.connect()
314        cursor  = conn.cursor()
315        # Number of session in the Last 7 days sessions
316        cursor.execute("SELECT date(createdOn) as day, COUNT(*) as occurrences FROM session WHERE createdon >= DATE_ADD(CURDATE(), INTERVAL -7 DAY) GROUP BY day")
317        res = cursor.fetchall()
318    except Exception as e:
319        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
320    
321    # Check for empty results 
322    if (len(res) == 0):
323        cursor.close()
324        conn.close()
325        if (not internal_call):
326            return(modules.utils.build_response_json(request.path, 200, {"size": 0}))
327        else:
328            return(None)   
329    else:
330        stat = {}
331        stat['top'] = []
332        for row in res:
333            date = {}
334            date['date'] = row[0]
335            date['occurrences'] = row[1]
336            stat['top'].append(date)
337
338    cursor.close()
339    conn.close()
340
341    # 'May the Force be with you, young master'.
342    if (not internal_call):
343        return(modules.utils.build_response_json(request.path, 200, stat))
344    else:
345        return(stat)
346
347
348"""
349[Summary]: Get the amount of elements in a table. 
350[Returns]: Integer.
351"""
352def get_number_of_table(table_name):
353    size = 0
354    try:
355        conn    = mysql.connect()
356        cursor  = conn.cursor()
357        cursor.execute("SELECT COUNT(id) as size FROM "+str(table_name))
358        res = cursor.fetchall()
359    except Exception as e:
360        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
361    
362    if len(res) != 0:
363            size = res[0][0]
364
365    return(size)
GET /api/statistics
Synopsis

Get the global statistics of the plataform.

Request Headers
Response Headers
Response JSON Object
  • answers (int) – The total number of responses.

  • modules (int) – The total number of modules.

  • recommendations (int) – The total number of recommendations.

  • sessions (int) – The total number of sessions executed by users.

  • users (int) – The total number of registered users.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/statistics":{
      "users":2
      "modules":2,
      "questions":30,
      "answers":60,
      "recommendations":10,
      "sessions":2,
      "status":200,
   }
}

GET /api/statistic/users
Synopsis

Get the total number of users of the platform.

Request Headers
Response Headers
Response JSON Object
  • size (int) – The total number of registered users.

Status Codes

Example Response

{"/api/statistic/users":{"size":2, "status":200}}

GET /api/statistic/modules
Synopsis

Get the total number of modules of the platform.

Request Headers
Response Headers
Response JSON Object
  • size (int) – The total number of modules.

  • top (array) – An array that contains the topmost used modules.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/statistic/modules":{
      "size":2,
      "top":[
         {
            "shortname":"SRE"
            "displayname":"Security Requirements",
            "occurrences":2,
         }
      ],
      "status":200
   }
}

GET /api/statistic/questions
Synopsis

Get the total number of questions of the platform.

Request Headers
Response Headers
Response JSON Object
  • size (int) – The total number of questions.

  • status (int) – Status code.

Status Codes

Example Response

{"/api/statistic/questions":{"size":30, "status":200}}

GET /api/statistic/answers
Synopsis

Get the total number of answers of the platform.

Request Headers
Response Headers
Response JSON Object
  • size (int) – The total number of answers.

  • status (int) – Status code.

Status Codes

Example Response

{"/api/statistic/answers":{"size":60, "status":200}}

GET /api/statistic/recommendations
Synopsis

Get the total number of recommendations of the platform.

Request Headers
Response Headers
Response JSON Object
  • size (int) – The total number of recommendations.

  • top (array) – An array that contains the topmost given recommendations.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/statistic/recommendations":{
      "size":10,
      "top":[
         {
            "content":"Confidentiality",
            "occurrences":2
         }
      ],
      "status":200
   }
}

GET /api/statistic/sessions
Synopsis

Get the total number of sessions per day created by users of the platform.

Request Headers
Response Headers
Response JSON Object
  • top (array) – An array that contains the number of sessions per day.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/statistic/sessions":{
      "top":[
         {
            "date":"Sat, 20 Nov 2021 00:00:00 GMT",
            "occurrences":2
         }
      ],
      "status":200
   }
}

Authentication Services API

This section includes details concerning services developed for the authentication entity implemented in
user.py.
  1"""
  2// ---------------------------------------------------------------------------
  3//
  4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
  5//
  6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
  7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
  8//
  9//  This program is free software: you can redistribute it and/or modify
 10//  it under the terms of the GNU General Public License as published by
 11//  the Free Software Foundation, either version 3 of the License, or
 12//  (at your option) any later version.
 13//
 14//  This program is distributed in the hope that it will be useful,
 15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
 16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17//  GNU General Public License for more details.
 18//
 19//  You should have received a copy of the GNU General Public License
 20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 21// 
 22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
 23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
 24//  POCI-01-0145-FEDER-030657) 
 25// ---------------------------------------------------------------------------
 26"""
 27import json, jwt, ast
 28from api import app, mysql, JWT_SECRET_TOKEN, JWT_EXPIRATION_SECONDS, RECAPTCHA_SECRET
 29from email_validator import validate_email, EmailNotValidError
 30from flask import request
 31from datetime import datetime
 32import requests
 33from datetime import timedelta
 34import modules.error_handlers, modules.utils # SAM's modules
 35
 36"""
 37[Summary]: User Authentication Service (i.e., login).
 38[Returns]: Returns a JSON object with the data of the user including a JWT authentication token.
 39"""
 40@app.route('/api/user/login', methods=['POST'])
 41def login_user():
 42    if request.method == "POST":
 43        # 1. Validate and parse POST data.
 44        if not request.form.get('email'):
 45            raise modules.error_handlers.BadRequest(request.path, 'The email cannot be empty', 400) 
 46        if not request.form.get('psw'):
 47            raise modules.error_handlers.BadRequest(request.path, 'The password cannot be empty', 400) 
 48        
 49        email   = request.form['email']
 50        psw     = request.form['psw']
 51
 52        # 2. Connect to the DB and get the user info.
 53        try:
 54            conn    = mysql.connect()
 55            cursor  = conn.cursor()
 56            cursor.execute("SELECT ID, email, psw, avatar, administrator FROM user WHERE email=%s", email)
 57        except Exception as e:
 58            raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
 59        # 2.1. Check if the user exists.
 60        if (cursor.rowcount == 0): 
 61            raise modules.error_handlers.BadRequest(request.path, "A user with the specified email was not found", 404) 
 62        
 63        # 2.2. Process the DB results.
 64        records = cursor.fetchall()
 65        dbpsw = ""
 66        data = {} # Create a new nice empty dictionary to be populated with data from the DB.
 67        for row in records:
 68            data['id']          = row[0]
 69            data['email']       = row[1]
 70            # For security reasons lets not store the password in the dic.
 71            dbpsw               = row[2] 
 72            data['avatar']      = row[3]
 73            data['is_admin']    = row[4]
 74            # Set the expiration time of the token, the JWT auth token will not be valid after x seconds
 75            # Default is a 15 minute session
 76            data['exp']     = datetime.utcnow() + timedelta(seconds=int(JWT_EXPIRATION_SECONDS))
 77
 78        cursor.close()
 79        conn.close()    
 80        
 81        # Check if the hashed password of the database is the same as the one provided by the user.
 82        if modules.utils.check_password(dbpsw, psw):
 83            # First, build the authentication token and added it to the dictionary.
 84            # Second, let's create a JSON response with the data of the dictionary.
 85            data['token'] = (jwt.encode(data, JWT_SECRET_TOKEN, algorithm='HS256')).decode('UTF-8')
 86            
 87            # Authentication success, the user 'can follow the white rabbit'.
 88            return (modules.utils.build_response_json(request.path, 200, data))
 89        else:
 90            raise modules.error_handlers.BadRequest(request.path, "Authentication failure", 401)
 91
 92"""
 93[Summary]: Clear the list of expired blacklisted JSON Web Tokens of a user.
 94[Arguments]:
 95       - $userID$: Target user.
 96[Returns]: Returns false, if an error occurs, true otherwise. 
 97"""
 98def clear_expired_blacklisted_JWT(userID):
 99    debug=False
100    if (debug): print("Checking blacklisted tokens for user id ="+str(userID))
101    try:
102        # Check if this user id exists.
103        conn    = mysql.connect()
104        cursor  = conn.cursor()
105        cursor.execute("SELECT token FROM auth_token_blackList WHERE userID=%s", userID)
106        res = cursor.fetchall()
107    except Exception as e:
108        if (debug): print(str(e))
109        return (False) # 'Houston, we have a problem.'
110    # Empty results ?
111    if (len(res) == 0):
112        cursor.close()
113        conn.close()
114        return (True)
115    else:
116        i=0
117        for row in res: 
118            token = row[0]
119            if (debug): print ("# Checking token[" + str(i) + "]" + "= " + token)
120            i = i + 1
121            try:
122                # Let's see if the token is expired
123                res_dic  = jwt.verify(token)
124                if (debug): print(" - The token is still 'alive'. Nothing to do here.")
125            except:
126                if (debug): print(" - The token is expired, removing it from the database for user with id " + str(userID))
127                # The token is expired, remove it from the DB
128                try:
129                    cursor.execute("DELETE FROM auth_token_blacklist WHERE token=%s AND userID=%s", (token,userID))
130                    conn.commit()
131                except Exception as e:
132                    if (debug): print(str(e))
133                    return (False) # 'Houston, we have a problem.'
134    return(True)
135
136"""
137[Summary]: Performs a logout using an JWT authentication token.
138[Returns]: 200 response if the user was successfully logged out.
139[ref]: Based on https://medium.com/devgorilla/how-to-log-out-when-using-jwt-a8c7823e8a6
140"""
141@app.route('/api/user/logout', methods=['POST'])
142def logout_user():
143    debug = True
144    
145    data = {}
146    if request.method != "POST": return
147    # 1. Check if the token is available on the request header.
148    headers = dict(request.headers)
149    if (debug): print(str(len(headers)))
150    
151    # 2. Check if the Authorization header name was parsed.
152    if 'Authorization' not in headers: raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the permission to access this resource. Please, provide an authorization token.", 403) 
153    token_parsed = headers['Authorization']
154    if ("Bearer" in token_parsed):
155        token_parsed = (token_parsed.replace("Bearer", "")).replace(" ", "")
156   
157    if (debug): print("Parsed Token:" + token_parsed)
158    
159    try:
160        # 3. From now on the token will be blacklisted because the user has logout and the token may still
161        #    exists somewhere because its expiration date is still valid.
162        
163        # 3.1. Let's clean the house - Remove possible expired tokens from the user of the token
164        res_dic  = jwt.decode(token_parsed, JWT_SECRET_TOKEN, algorithms=['HS256'])
165       
166        userID = int(res_dic['id'])
167        if not clear_expired_blacklisted_JWT(userID): # Sanity check
168            return (modules.utils.build_response_json(request.path, 500))
169        # 3.2. Let's add the token to the blacklist table on the database for the current user
170        #      That is, blacklist the current token, that may or may not be alive. 
171        conn    = mysql.connect()
172        cursor  = conn.cursor()
173        cursor.execute("INSERT INTO auth_token_blacklist (userID, token) VALUES (%s, %s)", (userID, token_parsed))
174        conn.commit()
175        data['message'] = "The authentication token was blacklisted. The user should now be logouted on the client side."
176       
177    except jwt.exceptions.ExpiredSignatureError as e:
178        # We don't need to blacklist this token, the token is already expired (see 'exp' field of the JWT defined in the authenticate function).
179        # raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
180        data['message'] = "The token has already expired, no need to blacklist it. The user should now be logouted on the client side."
181        return (modules.utils.build_response_json(request.path, 200, data))
182    except Exception as e :
183        # Double token entry ?
184        if e.args[0] == 1062:
185            data['message'] = "The token was already blacklisted. The user should now be logouted on the client side."
186        else:
187            raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
188    finally:
189        try:
190            cursor.close()
191            conn.close()
192        except:
193            pass # cursor or conn may not be defined, I don't care, 'Just keep swimming'.
194    #
195    return (modules.utils.build_response_json(request.path, 200, data))
196
197@app.route('/api/user/<email>/admin', methods=['GET'])
198def is_admin(email):
199    if request.method != 'GET': return
200    # 1. Check if the user has permissions to access this resource
201    isAuthenticated(request)
202
203    # 2. Let's get the groups associated with the parsed user.
204    try:
205        conn    = mysql.connect()
206        cursor  = conn.cursor()
207        cursor.execute("SELECT email, administrator FROM user WHERE email=%s AND administrator=1", email) 
208        res = cursor.fetchall()
209    except Exception as e:
210        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
211
212    # 2.2. Check for empty results 
213    if (len(res) == 0):
214        cursor.close()
215        conn.close()
216        return(modules.utils.build_response_json(request.path, 200, {"admin": 0})) 
217    else:
218        cursor.close()
219        conn.close()
220        # 3. 'May the Force be with you, young master'.
221        return(modules.utils.build_response_json(request.path, 200, {"admin": 1}))
222"""
223[Summary]: User Registration Service (i.e., add a new user).
224[Returns]: Returns a JSON object with the data of the user including a JWT authentication token.
225"""
226@app.route('/api/user', methods=['POST'])
227def add_user():
228    if request.method != 'POST': return
229
230    # 1. Let's get our shiny new JSON object and current time.
231    # - Always start by validating the structure of the json, if not valid send an invalid response.
232    try:
233        obj = request.json
234        obj=ast.literal_eval(str(obj).lower())
235
236        date = (datetime.now()).strftime('%Y-%m-%d %H:%M:%S')
237    except Exception as e:
238        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
239
240    # 2. Let's validate the data of our JSON object with a custom function.
241    if (not modules.utils.valid_json(obj, {"email", "psw", "firstname", "lastname", "avatar", "g-recaptcha-response"})):
242        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)
243
244    if ("insomnia" not in request.headers.get('User-Agent')):
245        # 3. Validate reCAPTCHA
246        if not is_human(obj['g-recaptcha-response']):
247            raise modules.error_handlers.BadRequest(request.path, "reCAPTCHA failure - Bots are not allowed.", 400)
248
249    # 4. Let's hash the hell of the password.
250    hashed_psw = modules.utils.hash_password(obj['psw'])
251    obj['psw'] = "" # "paranoic mode".
252    
253    # 5. Check if the user was not previously registered in the DB (i.e., same email)
254    if (find_user(obj['email']) is not None):
255        raise modules.error_handlers.BadRequest(request.path, "The user with that email already exists", 500)
256
257    # 6. Connect to the database and create the new user.
258    try:
259        conn    = mysql.connect()
260        cursor  = conn.cursor()
261        print(obj)
262        print(hashed_psw)
263        cursor.execute("INSERT INTO user (email, psw, firstName, lastName, avatar, createdon, updatedon) VALUES (%s, %s, %s, %s, %s, %s, %s)", (obj['email'], hashed_psw, obj['firstname'], obj['lastname'], obj['avatar'], date, date))
264        conn.commit()
265    except Exception as e:
266        print("--->" +str(e))
267        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
268    finally:
269        cursor.close()
270        conn.close()
271
272    # 7. Authentication success, the user can now choose to 'take the red or blue pill to follow the white rabbit'
273    return (modules.utils.build_response_json(request.path, 200))
274
275"""
276[Summary]: Get users.
277[Returns]: Returns a User object.
278"""
279# Check if a user exists
280@app.route('/api/users', methods=['GET'])
281def get_users():
282    if request.method != 'GET': return
283
284    # 1. Check if the user has permissions to access this resource
285    isAuthenticated(request)
286    
287    # 3. Let's get users from the database.
288    try:
289        conn    = mysql.connect()
290        cursor  = conn.cursor()
291        cursor.execute("SELECT ID, email, firstName, lastName, avatar, userStatus, administrator, createdon, updatedon FROM user")
292        res = cursor.fetchall()
293    except Exception as e:
294        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
295    
296    # Empty results ?
297    if (len(res) == 0):
298        cursor.close()
299        conn.close()
300        return(modules.utils.build_response_json(request.path, 404))    
301    else:
302        datas = [] 
303        for row in res:
304            data = {}
305            data['id']          = row[0]
306            data['email']       = row[1]
307            data['firstName']   = row[2]
308            data['lastName']    = row[3]
309            data['avatar']      = row[4]
310            data['user_status']  = row[5]
311            data['administrator'] = row[6]
312            data['createdon']   = row[7]
313            data['updatedon']   = row[8]
314            datas.append(data)
315        cursor.close()
316        conn.close()
317        # 4. Return information about the user (except the password) and 'May the Force be with you'.
318        return(modules.utils.build_response_json(request.path, 200, datas))    
319
320"""
321[Summary]: Finds a user by email.
322[Returns]: Returns a User object.
323"""
324# Check if a user exists
325@app.route('/api/user/<email>', methods=['GET'])
326def find_user(email, internal_call=False):
327    if (not internal_call):
328        if request.method != 'GET': return
329
330    # 1. Check if the user has permissions to access this resource
331    if (not internal_call):
332        isAuthenticated(request)
333       
334    # 2. Let's validate the email, invalid emails from this point are not allowed.
335    try:
336        valid = validate_email(email)
337    except EmailNotValidError as e:
338        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
339    
340    # 3. Let's get the user from the database with the provided [email].
341    try:
342        conn    = mysql.connect()
343        cursor  = conn.cursor()
344        cursor.execute("SELECT ID, email, firstName, lastName, avatar FROM user WHERE email=%s", email)
345        res = cursor.fetchall()
346    except Exception as e:
347        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
348    
349    # Empty results ?
350    if (len(res) == 0):
351        cursor.close()
352        conn.close()
353        if (not internal_call):
354            return(modules.utils.build_response_json(request.path, 404))
355        else:
356            return(None)
357    else:
358        data = {} # Create a new nice empty dictionary to be populated with data from the DB.
359        for row in res:
360            data['id']          = row[0]
361            data['email']       = row[1]
362            data['firstName']   = row[2]
363            data['lastName']    = row[3]
364            data['avatar']      = row[4]
365        cursor.close()
366        conn.close()
367
368        # 4. Return information about the user (except the password) and 'May the Force be with you'.
369        if (not internal_call):
370            return(modules.utils.build_response_json(request.path, 200, data))
371        else:
372            return(data)
373
374"""
375[Summary]: Delete a user
376[Returns]: Returns a success or error response
377"""
378@app.route('/api/user/<email>', methods=["DELETE"])
379def delete_user(email):
380    if request.method != 'DELETE': return
381    # 1. Check if the user has permissions to access this resource
382    isAuthenticated(request)
383  
384    # 2. Let's validate the email, invalid emails from this point are not allowed.
385    try:
386        valid = validate_email(email)
387    except EmailNotValidError as e:
388        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
389
390    # 3. Connect to the database and delete the user
391    # TODO: Let's build procedures in the DB to delete Users. 
392    try:
393        conn    = mysql.connect()
394        cursor  = conn.cursor()
395        cursor.execute("DELETE FROM user WHERE email=%s", email)
396        conn.commit()
397    except Exception as e:
398        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
399    finally:
400        cursor.close()
401        conn.close()
402
403    # 4. The Delete request was a success, the user 'took the blue pill'.
404    return (modules.utils.build_response_json(request.path, 200))
405
406"""
407[Summary]: Updates a user
408[Returns]: Returns a success or error response
409"""
410@app.route('/api/user/<email>', methods=["PUT"])
411def update_user(email):
412    updatePsw = False
413    # Note: Remember that if an email is being changed, the email argument is the old one; 
414    #       The new email content is available on the JSON object parsed in the body of the request.  
415    if request.method != 'PUT': return
416    # 1. Check if the user has permissions to access this resource
417    isAuthenticated(request)
418
419    # 2. Let's validate the email, invalid emails from this point are not allowed.
420    try:
421        valid = validate_email(email)
422    except EmailNotValidError as e:
423        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
424
425    # 3. Let's get our shiny new JSON object and current time.
426    # - Always start by validating the structure of the json, if not valid send an invalid response.
427    try:
428        obj = request.json
429        date = (datetime.now()).strftime('%Y-%m-%d %H:%M:%S')
430    except Exception as e:
431        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
432
433
434    # 4. Let's validate the data of our JSON object with a custom function.
435    if (not modules.utils.valid_json(obj, {"email", "avatar", "firstname", "lastname"})):
436        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)
437
438    # 4.1. Let's also validate the new email, invalid emails from this point are not allowed.
439    try:
440        valid = validate_email(obj['email'])
441    except EmailNotValidError as e:
442        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
443
444    # 5. Hash the new password and store it (if supplied)
445    if (modules.utils.valid_json(obj, {"psw"})):
446        updatePsw=True
447        hashed_psw = modules.utils.hash_password(obj['psw'])
448        obj['psw'] = "" # "paranoic mode".
449
450    # 6. Connect to the database and update the user with the data of the parsed json object
451    # TODO: - Let's build procedures in the DB to update Users. 
452    #       - Let's not update every single field of the User, instead, let's just updated the one that has changed.
453    try:
454        conn    = mysql.connect()
455        cursor  = conn.cursor()
456        if (updatePsw):
457            cursor.execute("UPDATE user SET email=%s, psw=%s, firstName=%s, lastName=%s, avatar=%s, updatedOn=%s WHERE email=%s",  (obj['email'], hashed_psw, obj['firstname'], obj['lastname'], obj['avatar'],date,email))
458        else:
459            cursor.execute("UPDATE user SET email=%s, firstName=%s, lastName=%s, avatar=%s, updatedOn=%s WHERE email=%s",  (obj['email'], obj['firstname'], obj['lastname'], obj['avatar'],date,email))    
460        conn.commit()
461    except Exception as e:
462        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
463    finally:
464        cursor.close()
465        conn.close()
466
467    # 4. The Update request was a success, the user 'is in the rabbit hole'
468    return (modules.utils.build_response_json(request.path, 200))
469
470"""
471[Summary]: Finds groups of a user.
472[Returns]: Returns a success or error response
473"""
474@app.route('/api/user/<email>/groups', methods=["GET"])
475def find_user_groups(email):
476    if request.method != 'GET': return
477
478    # 1. Check if the user has permissions to access this resource
479    isAuthenticated(request)
480       
481    # 2. Let's validate the email, invalid emails from this point are not allowed.
482    try:
483        valid = validate_email(email)
484    except EmailNotValidError as e:
485        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
486    
487    # 3. Let's get the user from the database with the provided [email].
488    try:
489        conn    = mysql.connect()
490        cursor  = conn.cursor()
491        cursor.execute("SELECT user_id, user_email, user_group FROM view_user_group WHERE user_email=%s", email)
492        res = cursor.fetchall()
493    except Exception as e:
494        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
495    
496    # Empty results ?
497    if (len(res) == 0):
498        cursor.close()
499        conn.close()
500        return(modules.utils.build_response_json(request.path, 404))    
501    else:
502        datas = []
503        for row in res:
504            data = {} # Create a new nice empty dictionary to be populated with data from the DB.
505            data['user_id']     = row[0]
506            data['user_email']  = row[1]
507            data['user_group']  = row[2]
508            datas.append(data)
509        cursor.close()
510        conn.close()
511        # 4. Return information about the user (except the password) and 'May the Force be with you'.
512        return(modules.utils.build_response_json(request.path, 200, datas))    
513
514""" [Summary]: Validates recaptcha response from google server.
515    [Returns]: Returns True captcha test passed, false otherwise.
516    [TODO]: In a production environment the client and server key should be reconfigured.
517"""
518def is_human(captcha_response):
519    # https://www.google.com/recaptcha/
520    secret = RECAPTCHA_SECRET
521    payload = {'response':captcha_response, 'secret':secret}
522    response = requests.post("https://www.google.com/recaptcha/api/siteverify", payload)
523    response_text = json.loads(response.text)
524    return response_text['success']
525
526"""
527[Summary]: Check if the user has the necessary permissions to access a service.
528[Returns]: True if access is granted to access the resource, false otherwise.
529[ref]: CHECK THIS: https://medium.com/devgorilla/how-to-log-out-when-using-jwt-a8c7823e8a6
530"""
531def isAuthenticated(request):
532    # 1. Check if the token is available on the request header.
533    headers = dict(request.headers)
534    # Debug only: print(str(len(headers)))
535    
536    # 2. Check if the Authorization header name was parsed.
537    if 'Authorization' not in headers: raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the permission to access this resource. Please, provide an authorization token.", 403) 
538    parsedToken = headers['Authorization']
539    if ("Bearer" in parsedToken):
540        parsedToken = (parsedToken.replace("Bearer", "")).replace(" ", "")
541    
542    # 3. Decode the authorization token to get the User object.
543    try:
544        # Decode will raise an exception if anything goes wrong within the decoding process (i.e., perform validation of the JWT).
545        res_dic  = jwt.decode(parsedToken, JWT_SECRET_TOKEN, algorithms=['HS256'])
546        # Get the ID of the user.
547        userID = int(res_dic['id'])
548        # Debug only: print(str(json.dumps(res_dic)))
549       
550        conn    = mysql.connect()
551        cursor  = conn.cursor()
552        # 3.1. Check if the token is not blacklisted, that is if a user was previously logged out from the platform but the token is still 'alive'.
553        cursor.execute("SELECT ID FROM auth_token_blacklist WHERE userID=%s AND token=%s", (userID, parsedToken))
554        res = cursor.fetchall()
555        if (len(res) == 1):
556            raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the necessary permissions to access this resource. Please, provide an authorization token.", 403) 
557        cursor.close()
558        cursor  = conn.cursor()
559
560        # 3.2. Get info of the user
561        # Check if this user id exists.
562        cursor.execute("SELECT ID FROM user WHERE id=%s", userID)
563        res = cursor.fetchall()
564    except Exception as e:
565        raise modules.error_handlers.BadRequest(request.path, str(e), 403) 
566
567    if (len(res) == 1): 
568        cursor.close()
569        conn.close()    
570        return True # The user is legit, we can let him access the target resource 
571    else:
572        cursor.close()
573        conn.close()    
574        raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the necessary permissions to access this resource. Please, provide an authorization token.", 403) 
575
576""" TODO: Merge this function with the previous one. This function is used to make sure that only admins access particular services."""
577def isAuthenticatedAdmin(request):
578    # 1. Check if the token is available on the request header.
579    headers = dict(request.headers)
580    # Debug only: print(str(len(headers)))
581    
582    # 2. Check if the Authorization header name was parsed.
583    if 'Authorization' not in headers: raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the permission to access this resource. Please, provide an authorization token.", 403) 
584    parsedToken = headers['Authorization']
585    if ("Bearer" in parsedToken):
586        parsedToken = (parsedToken.replace("Bearer", "")).replace(" ", "")
587    
588    # 3. Decode the authorization token to get the User object.
589    try:
590        # Decode will raise an exception if anything goes wrong within the decoding process (i.e., perform validation of the JWT).
591        res_dic  = jwt.decode(parsedToken, JWT_SECRET_TOKEN, algorithms=['HS256'])
592        # Get the ID of the user.
593        userID = int(res_dic['id'])
594        # Debug only: print(str(json.dumps(res_dic)))
595        # The user is not an administrator
596        if ( int(res_dic['is_admin']) == 0):
597            raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the permission to access this resource.", 403) 
598        
599        conn    = mysql.connect()
600        cursor  = conn.cursor()
601        # 3.1. Check if the token is not blacklisted, that is if a user was previously logged out from the platform but the token is still 'alive'.
602        cursor.execute("SELECT ID FROM auth_token_blacklist WHERE userID=%s AND token=%s", (userID, parsedToken))
603        res = cursor.fetchall()
604        if (len(res) == 1):
605            raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the necessary permissions to access this resource. Please, provide an authorization token.", 403) 
606        cursor.close()
607        cursor  = conn.cursor()
608
609        # 3.2. Get info of the user
610        # Check if this user id exists.
611        cursor.execute("SELECT ID FROM user WHERE id=%s", userID)
612        res = cursor.fetchall()
613    except Exception as e:
614        raise modules.error_handlers.BadRequest(request.path, str(e), 403) 
615
616    if (len(res) == 1): 
617        cursor.close()
618        conn.close()    
619        return True # The user is legit, we can let him access the target resource 
620    else:
621        cursor.close()
622        conn.close()    
623        raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the necessary permissions to access this resource. Please, provide an authorization token.", 403) 

Authenticate User

POST /api/user/login
Synopsis

Authenticate user with an email and password. The authentication process returns a bearer JWT used to grant users access to SAM’s Web services.

Response Headers
Form Parameters
  • email – The email of the user to be authenticated.

  • psw – The user password.

Response JSON Object
  • id (int) – The id of the user.

  • email (string) – The email of the user.

  • avatar (string) – Avatar of the user (i.e., location in disk).

  • is_admin (boolean) – Is the user an administrator.

  • exp (int) – Expiration time of the token in seconds.

  • token (string) – The bearer JWT.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/user/login":{
      "id":1,
      "email":"forrest@sam.pt",
      "avatar":null,
      "is_admin":0,
      "exp":1637410963,
      "token":"eyJ0eXAiOiJKV1QiLCJhb..."
      "status":200,
   }
}

Note

By default the JSON Web Token (JWT) authentication token will expire after 15 minutes.

Logout User

POST /api/user/logout
Synopsis

Logout user with the provided authentication token that should be available in the Authorization request header.

Request Headers
Response JSON Object
  • status (int) – status code.

Status Codes

Example Response

{
   "/api/user/logout":{
      "status":200
   }
}

User Services API

This section includes details concerning services developed for the group entity implemented in
user.py.
  1"""
  2// ---------------------------------------------------------------------------
  3//
  4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
  5//
  6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
  7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
  8//
  9//  This program is free software: you can redistribute it and/or modify
 10//  it under the terms of the GNU General Public License as published by
 11//  the Free Software Foundation, either version 3 of the License, or
 12//  (at your option) any later version.
 13//
 14//  This program is distributed in the hope that it will be useful,
 15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
 16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17//  GNU General Public License for more details.
 18//
 19//  You should have received a copy of the GNU General Public License
 20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 21// 
 22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
 23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
 24//  POCI-01-0145-FEDER-030657) 
 25// ---------------------------------------------------------------------------
 26"""
 27import json, jwt, ast
 28from api import app, mysql, JWT_SECRET_TOKEN, JWT_EXPIRATION_SECONDS, RECAPTCHA_SECRET
 29from email_validator import validate_email, EmailNotValidError
 30from flask import request
 31from datetime import datetime
 32import requests
 33from datetime import timedelta
 34import modules.error_handlers, modules.utils # SAM's modules
 35
 36"""
 37[Summary]: User Authentication Service (i.e., login).
 38[Returns]: Returns a JSON object with the data of the user including a JWT authentication token.
 39"""
 40@app.route('/api/user/login', methods=['POST'])
 41def login_user():
 42    if request.method == "POST":
 43        # 1. Validate and parse POST data.
 44        if not request.form.get('email'):
 45            raise modules.error_handlers.BadRequest(request.path, 'The email cannot be empty', 400) 
 46        if not request.form.get('psw'):
 47            raise modules.error_handlers.BadRequest(request.path, 'The password cannot be empty', 400) 
 48        
 49        email   = request.form['email']
 50        psw     = request.form['psw']
 51
 52        # 2. Connect to the DB and get the user info.
 53        try:
 54            conn    = mysql.connect()
 55            cursor  = conn.cursor()
 56            cursor.execute("SELECT ID, email, psw, avatar, administrator FROM user WHERE email=%s", email)
 57        except Exception as e:
 58            raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
 59        # 2.1. Check if the user exists.
 60        if (cursor.rowcount == 0): 
 61            raise modules.error_handlers.BadRequest(request.path, "A user with the specified email was not found", 404) 
 62        
 63        # 2.2. Process the DB results.
 64        records = cursor.fetchall()
 65        dbpsw = ""
 66        data = {} # Create a new nice empty dictionary to be populated with data from the DB.
 67        for row in records:
 68            data['id']          = row[0]
 69            data['email']       = row[1]
 70            # For security reasons lets not store the password in the dic.
 71            dbpsw               = row[2] 
 72            data['avatar']      = row[3]
 73            data['is_admin']    = row[4]
 74            # Set the expiration time of the token, the JWT auth token will not be valid after x seconds
 75            # Default is a 15 minute session
 76            data['exp']     = datetime.utcnow() + timedelta(seconds=int(JWT_EXPIRATION_SECONDS))
 77
 78        cursor.close()
 79        conn.close()    
 80        
 81        # Check if the hashed password of the database is the same as the one provided by the user.
 82        if modules.utils.check_password(dbpsw, psw):
 83            # First, build the authentication token and added it to the dictionary.
 84            # Second, let's create a JSON response with the data of the dictionary.
 85            data['token'] = (jwt.encode(data, JWT_SECRET_TOKEN, algorithm='HS256')).decode('UTF-8')
 86            
 87            # Authentication success, the user 'can follow the white rabbit'.
 88            return (modules.utils.build_response_json(request.path, 200, data))
 89        else:
 90            raise modules.error_handlers.BadRequest(request.path, "Authentication failure", 401)
 91
 92"""
 93[Summary]: Clear the list of expired blacklisted JSON Web Tokens of a user.
 94[Arguments]:
 95       - $userID$: Target user.
 96[Returns]: Returns false, if an error occurs, true otherwise. 
 97"""
 98def clear_expired_blacklisted_JWT(userID):
 99    debug=False
100    if (debug): print("Checking blacklisted tokens for user id ="+str(userID))
101    try:
102        # Check if this user id exists.
103        conn    = mysql.connect()
104        cursor  = conn.cursor()
105        cursor.execute("SELECT token FROM auth_token_blackList WHERE userID=%s", userID)
106        res = cursor.fetchall()
107    except Exception as e:
108        if (debug): print(str(e))
109        return (False) # 'Houston, we have a problem.'
110    # Empty results ?
111    if (len(res) == 0):
112        cursor.close()
113        conn.close()
114        return (True)
115    else:
116        i=0
117        for row in res: 
118            token = row[0]
119            if (debug): print ("# Checking token[" + str(i) + "]" + "= " + token)
120            i = i + 1
121            try:
122                # Let's see if the token is expired
123                res_dic  = jwt.verify(token)
124                if (debug): print(" - The token is still 'alive'. Nothing to do here.")
125            except:
126                if (debug): print(" - The token is expired, removing it from the database for user with id " + str(userID))
127                # The token is expired, remove it from the DB
128                try:
129                    cursor.execute("DELETE FROM auth_token_blacklist WHERE token=%s AND userID=%s", (token,userID))
130                    conn.commit()
131                except Exception as e:
132                    if (debug): print(str(e))
133                    return (False) # 'Houston, we have a problem.'
134    return(True)
135
136"""
137[Summary]: Performs a logout using an JWT authentication token.
138[Returns]: 200 response if the user was successfully logged out.
139[ref]: Based on https://medium.com/devgorilla/how-to-log-out-when-using-jwt-a8c7823e8a6
140"""
141@app.route('/api/user/logout', methods=['POST'])
142def logout_user():
143    debug = True
144    
145    data = {}
146    if request.method != "POST": return
147    # 1. Check if the token is available on the request header.
148    headers = dict(request.headers)
149    if (debug): print(str(len(headers)))
150    
151    # 2. Check if the Authorization header name was parsed.
152    if 'Authorization' not in headers: raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the permission to access this resource. Please, provide an authorization token.", 403) 
153    token_parsed = headers['Authorization']
154    if ("Bearer" in token_parsed):
155        token_parsed = (token_parsed.replace("Bearer", "")).replace(" ", "")
156   
157    if (debug): print("Parsed Token:" + token_parsed)
158    
159    try:
160        # 3. From now on the token will be blacklisted because the user has logout and the token may still
161        #    exists somewhere because its expiration date is still valid.
162        
163        # 3.1. Let's clean the house - Remove possible expired tokens from the user of the token
164        res_dic  = jwt.decode(token_parsed, JWT_SECRET_TOKEN, algorithms=['HS256'])
165       
166        userID = int(res_dic['id'])
167        if not clear_expired_blacklisted_JWT(userID): # Sanity check
168            return (modules.utils.build_response_json(request.path, 500))
169        # 3.2. Let's add the token to the blacklist table on the database for the current user
170        #      That is, blacklist the current token, that may or may not be alive. 
171        conn    = mysql.connect()
172        cursor  = conn.cursor()
173        cursor.execute("INSERT INTO auth_token_blacklist (userID, token) VALUES (%s, %s)", (userID, token_parsed))
174        conn.commit()
175        data['message'] = "The authentication token was blacklisted. The user should now be logouted on the client side."
176       
177    except jwt.exceptions.ExpiredSignatureError as e:
178        # We don't need to blacklist this token, the token is already expired (see 'exp' field of the JWT defined in the authenticate function).
179        # raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
180        data['message'] = "The token has already expired, no need to blacklist it. The user should now be logouted on the client side."
181        return (modules.utils.build_response_json(request.path, 200, data))
182    except Exception as e :
183        # Double token entry ?
184        if e.args[0] == 1062:
185            data['message'] = "The token was already blacklisted. The user should now be logouted on the client side."
186        else:
187            raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
188    finally:
189        try:
190            cursor.close()
191            conn.close()
192        except:
193            pass # cursor or conn may not be defined, I don't care, 'Just keep swimming'.
194    #
195    return (modules.utils.build_response_json(request.path, 200, data))
196
197@app.route('/api/user/<email>/admin', methods=['GET'])
198def is_admin(email):
199    if request.method != 'GET': return
200    # 1. Check if the user has permissions to access this resource
201    isAuthenticated(request)
202
203    # 2. Let's get the groups associated with the parsed user.
204    try:
205        conn    = mysql.connect()
206        cursor  = conn.cursor()
207        cursor.execute("SELECT email, administrator FROM user WHERE email=%s AND administrator=1", email) 
208        res = cursor.fetchall()
209    except Exception as e:
210        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
211
212    # 2.2. Check for empty results 
213    if (len(res) == 0):
214        cursor.close()
215        conn.close()
216        return(modules.utils.build_response_json(request.path, 200, {"admin": 0})) 
217    else:
218        cursor.close()
219        conn.close()
220        # 3. 'May the Force be with you, young master'.
221        return(modules.utils.build_response_json(request.path, 200, {"admin": 1}))
222"""
223[Summary]: User Registration Service (i.e., add a new user).
224[Returns]: Returns a JSON object with the data of the user including a JWT authentication token.
225"""
226@app.route('/api/user', methods=['POST'])
227def add_user():
228    if request.method != 'POST': return
229
230    # 1. Let's get our shiny new JSON object and current time.
231    # - Always start by validating the structure of the json, if not valid send an invalid response.
232    try:
233        obj = request.json
234        obj=ast.literal_eval(str(obj).lower())
235
236        date = (datetime.now()).strftime('%Y-%m-%d %H:%M:%S')
237    except Exception as e:
238        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
239
240    # 2. Let's validate the data of our JSON object with a custom function.
241    if (not modules.utils.valid_json(obj, {"email", "psw", "firstname", "lastname", "avatar", "g-recaptcha-response"})):
242        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)
243
244    if ("insomnia" not in request.headers.get('User-Agent')):
245        # 3. Validate reCAPTCHA
246        if not is_human(obj['g-recaptcha-response']):
247            raise modules.error_handlers.BadRequest(request.path, "reCAPTCHA failure - Bots are not allowed.", 400)
248
249    # 4. Let's hash the hell of the password.
250    hashed_psw = modules.utils.hash_password(obj['psw'])
251    obj['psw'] = "" # "paranoic mode".
252    
253    # 5. Check if the user was not previously registered in the DB (i.e., same email)
254    if (find_user(obj['email']) is not None):
255        raise modules.error_handlers.BadRequest(request.path, "The user with that email already exists", 500)
256
257    # 6. Connect to the database and create the new user.
258    try:
259        conn    = mysql.connect()
260        cursor  = conn.cursor()
261        print(obj)
262        print(hashed_psw)
263        cursor.execute("INSERT INTO user (email, psw, firstName, lastName, avatar, createdon, updatedon) VALUES (%s, %s, %s, %s, %s, %s, %s)", (obj['email'], hashed_psw, obj['firstname'], obj['lastname'], obj['avatar'], date, date))
264        conn.commit()
265    except Exception as e:
266        print("--->" +str(e))
267        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
268    finally:
269        cursor.close()
270        conn.close()
271
272    # 7. Authentication success, the user can now choose to 'take the red or blue pill to follow the white rabbit'
273    return (modules.utils.build_response_json(request.path, 200))
274
275"""
276[Summary]: Get users.
277[Returns]: Returns a User object.
278"""
279# Check if a user exists
280@app.route('/api/users', methods=['GET'])
281def get_users():
282    if request.method != 'GET': return
283
284    # 1. Check if the user has permissions to access this resource
285    isAuthenticated(request)
286    
287    # 3. Let's get users from the database.
288    try:
289        conn    = mysql.connect()
290        cursor  = conn.cursor()
291        cursor.execute("SELECT ID, email, firstName, lastName, avatar, userStatus, administrator, createdon, updatedon FROM user")
292        res = cursor.fetchall()
293    except Exception as e:
294        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
295    
296    # Empty results ?
297    if (len(res) == 0):
298        cursor.close()
299        conn.close()
300        return(modules.utils.build_response_json(request.path, 404))    
301    else:
302        datas = [] 
303        for row in res:
304            data = {}
305            data['id']          = row[0]
306            data['email']       = row[1]
307            data['firstName']   = row[2]
308            data['lastName']    = row[3]
309            data['avatar']      = row[4]
310            data['user_status']  = row[5]
311            data['administrator'] = row[6]
312            data['createdon']   = row[7]
313            data['updatedon']   = row[8]
314            datas.append(data)
315        cursor.close()
316        conn.close()
317        # 4. Return information about the user (except the password) and 'May the Force be with you'.
318        return(modules.utils.build_response_json(request.path, 200, datas))    
319
320"""
321[Summary]: Finds a user by email.
322[Returns]: Returns a User object.
323"""
324# Check if a user exists
325@app.route('/api/user/<email>', methods=['GET'])
326def find_user(email, internal_call=False):
327    if (not internal_call):
328        if request.method != 'GET': return
329
330    # 1. Check if the user has permissions to access this resource
331    if (not internal_call):
332        isAuthenticated(request)
333       
334    # 2. Let's validate the email, invalid emails from this point are not allowed.
335    try:
336        valid = validate_email(email)
337    except EmailNotValidError as e:
338        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
339    
340    # 3. Let's get the user from the database with the provided [email].
341    try:
342        conn    = mysql.connect()
343        cursor  = conn.cursor()
344        cursor.execute("SELECT ID, email, firstName, lastName, avatar FROM user WHERE email=%s", email)
345        res = cursor.fetchall()
346    except Exception as e:
347        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
348    
349    # Empty results ?
350    if (len(res) == 0):
351        cursor.close()
352        conn.close()
353        if (not internal_call):
354            return(modules.utils.build_response_json(request.path, 404))
355        else:
356            return(None)
357    else:
358        data = {} # Create a new nice empty dictionary to be populated with data from the DB.
359        for row in res:
360            data['id']          = row[0]
361            data['email']       = row[1]
362            data['firstName']   = row[2]
363            data['lastName']    = row[3]
364            data['avatar']      = row[4]
365        cursor.close()
366        conn.close()
367
368        # 4. Return information about the user (except the password) and 'May the Force be with you'.
369        if (not internal_call):
370            return(modules.utils.build_response_json(request.path, 200, data))
371        else:
372            return(data)
373
374"""
375[Summary]: Delete a user
376[Returns]: Returns a success or error response
377"""
378@app.route('/api/user/<email>', methods=["DELETE"])
379def delete_user(email):
380    if request.method != 'DELETE': return
381    # 1. Check if the user has permissions to access this resource
382    isAuthenticated(request)
383  
384    # 2. Let's validate the email, invalid emails from this point are not allowed.
385    try:
386        valid = validate_email(email)
387    except EmailNotValidError as e:
388        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
389
390    # 3. Connect to the database and delete the user
391    # TODO: Let's build procedures in the DB to delete Users. 
392    try:
393        conn    = mysql.connect()
394        cursor  = conn.cursor()
395        cursor.execute("DELETE FROM user WHERE email=%s", email)
396        conn.commit()
397    except Exception as e:
398        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
399    finally:
400        cursor.close()
401        conn.close()
402
403    # 4. The Delete request was a success, the user 'took the blue pill'.
404    return (modules.utils.build_response_json(request.path, 200))
405
406"""
407[Summary]: Updates a user
408[Returns]: Returns a success or error response
409"""
410@app.route('/api/user/<email>', methods=["PUT"])
411def update_user(email):
412    updatePsw = False
413    # Note: Remember that if an email is being changed, the email argument is the old one; 
414    #       The new email content is available on the JSON object parsed in the body of the request.  
415    if request.method != 'PUT': return
416    # 1. Check if the user has permissions to access this resource
417    isAuthenticated(request)
418
419    # 2. Let's validate the email, invalid emails from this point are not allowed.
420    try:
421        valid = validate_email(email)
422    except EmailNotValidError as e:
423        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
424
425    # 3. Let's get our shiny new JSON object and current time.
426    # - Always start by validating the structure of the json, if not valid send an invalid response.
427    try:
428        obj = request.json
429        date = (datetime.now()).strftime('%Y-%m-%d %H:%M:%S')
430    except Exception as e:
431        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
432
433
434    # 4. Let's validate the data of our JSON object with a custom function.
435    if (not modules.utils.valid_json(obj, {"email", "avatar", "firstname", "lastname"})):
436        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)
437
438    # 4.1. Let's also validate the new email, invalid emails from this point are not allowed.
439    try:
440        valid = validate_email(obj['email'])
441    except EmailNotValidError as e:
442        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
443
444    # 5. Hash the new password and store it (if supplied)
445    if (modules.utils.valid_json(obj, {"psw"})):
446        updatePsw=True
447        hashed_psw = modules.utils.hash_password(obj['psw'])
448        obj['psw'] = "" # "paranoic mode".
449
450    # 6. Connect to the database and update the user with the data of the parsed json object
451    # TODO: - Let's build procedures in the DB to update Users. 
452    #       - Let's not update every single field of the User, instead, let's just updated the one that has changed.
453    try:
454        conn    = mysql.connect()
455        cursor  = conn.cursor()
456        if (updatePsw):
457            cursor.execute("UPDATE user SET email=%s, psw=%s, firstName=%s, lastName=%s, avatar=%s, updatedOn=%s WHERE email=%s",  (obj['email'], hashed_psw, obj['firstname'], obj['lastname'], obj['avatar'],date,email))
458        else:
459            cursor.execute("UPDATE user SET email=%s, firstName=%s, lastName=%s, avatar=%s, updatedOn=%s WHERE email=%s",  (obj['email'], obj['firstname'], obj['lastname'], obj['avatar'],date,email))    
460        conn.commit()
461    except Exception as e:
462        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
463    finally:
464        cursor.close()
465        conn.close()
466
467    # 4. The Update request was a success, the user 'is in the rabbit hole'
468    return (modules.utils.build_response_json(request.path, 200))
469
470"""
471[Summary]: Finds groups of a user.
472[Returns]: Returns a success or error response
473"""
474@app.route('/api/user/<email>/groups', methods=["GET"])
475def find_user_groups(email):
476    if request.method != 'GET': return
477
478    # 1. Check if the user has permissions to access this resource
479    isAuthenticated(request)
480       
481    # 2. Let's validate the email, invalid emails from this point are not allowed.
482    try:
483        valid = validate_email(email)
484    except EmailNotValidError as e:
485        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
486    
487    # 3. Let's get the user from the database with the provided [email].
488    try:
489        conn    = mysql.connect()
490        cursor  = conn.cursor()
491        cursor.execute("SELECT user_id, user_email, user_group FROM view_user_group WHERE user_email=%s", email)
492        res = cursor.fetchall()
493    except Exception as e:
494        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
495    
496    # Empty results ?
497    if (len(res) == 0):
498        cursor.close()
499        conn.close()
500        return(modules.utils.build_response_json(request.path, 404))    
501    else:
502        datas = []
503        for row in res:
504            data = {} # Create a new nice empty dictionary to be populated with data from the DB.
505            data['user_id']     = row[0]
506            data['user_email']  = row[1]
507            data['user_group']  = row[2]
508            datas.append(data)
509        cursor.close()
510        conn.close()
511        # 4. Return information about the user (except the password) and 'May the Force be with you'.
512        return(modules.utils.build_response_json(request.path, 200, datas))    
513
514""" [Summary]: Validates recaptcha response from google server.
515    [Returns]: Returns True captcha test passed, false otherwise.
516    [TODO]: In a production environment the client and server key should be reconfigured.
517"""
518def is_human(captcha_response):
519    # https://www.google.com/recaptcha/
520    secret = RECAPTCHA_SECRET
521    payload = {'response':captcha_response, 'secret':secret}
522    response = requests.post("https://www.google.com/recaptcha/api/siteverify", payload)
523    response_text = json.loads(response.text)
524    return response_text['success']
525
526"""
527[Summary]: Check if the user has the necessary permissions to access a service.
528[Returns]: True if access is granted to access the resource, false otherwise.
529[ref]: CHECK THIS: https://medium.com/devgorilla/how-to-log-out-when-using-jwt-a8c7823e8a6
530"""
531def isAuthenticated(request):
532    # 1. Check if the token is available on the request header.
533    headers = dict(request.headers)
534    # Debug only: print(str(len(headers)))
535    
536    # 2. Check if the Authorization header name was parsed.
537    if 'Authorization' not in headers: raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the permission to access this resource. Please, provide an authorization token.", 403) 
538    parsedToken = headers['Authorization']
539    if ("Bearer" in parsedToken):
540        parsedToken = (parsedToken.replace("Bearer", "")).replace(" ", "")
541    
542    # 3. Decode the authorization token to get the User object.
543    try:
544        # Decode will raise an exception if anything goes wrong within the decoding process (i.e., perform validation of the JWT).
545        res_dic  = jwt.decode(parsedToken, JWT_SECRET_TOKEN, algorithms=['HS256'])
546        # Get the ID of the user.
547        userID = int(res_dic['id'])
548        # Debug only: print(str(json.dumps(res_dic)))
549       
550        conn    = mysql.connect()
551        cursor  = conn.cursor()
552        # 3.1. Check if the token is not blacklisted, that is if a user was previously logged out from the platform but the token is still 'alive'.
553        cursor.execute("SELECT ID FROM auth_token_blacklist WHERE userID=%s AND token=%s", (userID, parsedToken))
554        res = cursor.fetchall()
555        if (len(res) == 1):
556            raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the necessary permissions to access this resource. Please, provide an authorization token.", 403) 
557        cursor.close()
558        cursor  = conn.cursor()
559
560        # 3.2. Get info of the user
561        # Check if this user id exists.
562        cursor.execute("SELECT ID FROM user WHERE id=%s", userID)
563        res = cursor.fetchall()
564    except Exception as e:
565        raise modules.error_handlers.BadRequest(request.path, str(e), 403) 
566
567    if (len(res) == 1): 
568        cursor.close()
569        conn.close()    
570        return True # The user is legit, we can let him access the target resource 
571    else:
572        cursor.close()
573        conn.close()    
574        raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the necessary permissions to access this resource. Please, provide an authorization token.", 403) 
575
576""" TODO: Merge this function with the previous one. This function is used to make sure that only admins access particular services."""
577def isAuthenticatedAdmin(request):
578    # 1. Check if the token is available on the request header.
579    headers = dict(request.headers)
580    # Debug only: print(str(len(headers)))
581    
582    # 2. Check if the Authorization header name was parsed.
583    if 'Authorization' not in headers: raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the permission to access this resource. Please, provide an authorization token.", 403) 
584    parsedToken = headers['Authorization']
585    if ("Bearer" in parsedToken):
586        parsedToken = (parsedToken.replace("Bearer", "")).replace(" ", "")
587    
588    # 3. Decode the authorization token to get the User object.
589    try:
590        # Decode will raise an exception if anything goes wrong within the decoding process (i.e., perform validation of the JWT).
591        res_dic  = jwt.decode(parsedToken, JWT_SECRET_TOKEN, algorithms=['HS256'])
592        # Get the ID of the user.
593        userID = int(res_dic['id'])
594        # Debug only: print(str(json.dumps(res_dic)))
595        # The user is not an administrator
596        if ( int(res_dic['is_admin']) == 0):
597            raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the permission to access this resource.", 403) 
598        
599        conn    = mysql.connect()
600        cursor  = conn.cursor()
601        # 3.1. Check if the token is not blacklisted, that is if a user was previously logged out from the platform but the token is still 'alive'.
602        cursor.execute("SELECT ID FROM auth_token_blacklist WHERE userID=%s AND token=%s", (userID, parsedToken))
603        res = cursor.fetchall()
604        if (len(res) == 1):
605            raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the necessary permissions to access this resource. Please, provide an authorization token.", 403) 
606        cursor.close()
607        cursor  = conn.cursor()
608
609        # 3.2. Get info of the user
610        # Check if this user id exists.
611        cursor.execute("SELECT ID FROM user WHERE id=%s", userID)
612        res = cursor.fetchall()
613    except Exception as e:
614        raise modules.error_handlers.BadRequest(request.path, str(e), 403) 
615
616    if (len(res) == 1): 
617        cursor.close()
618        conn.close()    
619        return True # The user is legit, we can let him access the target resource 
620    else:
621        cursor.close()
622        conn.close()    
623        raise modules.error_handlers.BadRequest(request.path, "Authentication failure - You don't have the necessary permissions to access this resource. Please, provide an authorization token.", 403) 

Get Users

GET /api/user
Synopsis

Get all users

Request Headers
Response Headers
Response JSON Object
  • id (int) – Id of the user.

  • email (string) – email of the user.

  • avatar (string) – avatar of the user (i.e., location in disk).

  • firstname (string) – First name of the user.

  • lastname (string) – Last name of the user.

  • user_status (boolean) – Is the user active.

  • administrator (boolean) – Is the user an admin.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/users":{
      "content":[
         {
            "administrator":1,
            "avatar":null,
            "createdon":"Thu, 18 Nov 2021 12:38:43 GMT",
            "email":"admin@sam.pt",
            "firstName":"Administrator",
            "id":1,
            "lastName":null,
            "updatedon":"Thu, 18 Nov 2021 12:38:43 GMT",
            "user_status":1
         },
         {
            "administrator":0,
            "avatar":null,
            "createdon":"Thu, 18 Nov 2021 12:38:43 GMT",
            "email":"forrest@sam.pt",
            "firstName":"Forrest",
            "id":2,
            "lastName":"Gump",
            "updatedon":"Thu, 18 Nov 2021 12:38:43 GMT",
            "user_status":1
         }
      ],
      "status":200
   }
}

GET /api/user/(string: email)
Synopsis

Get a user by email.

Request Headers
Response Headers
Parameters
  • email – The email of the user to be retrieved.

Response JSON Object
  • id (int) – Id of the user.

  • email (string) – email of the user.

  • avatar (string) – avatar of the user (i.e., location in disk).

  • firstname (string) – First name of the user.

  • lastname (string) – Last name of the user.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/user/forrest@sam.pt":{
      "avatar":null,
      "email":"forrest@sam.pt",
      "firstName":"Forrest",
      "id":2,
      "lastName":"Gump",
      "status":200
   }
}

Get User Groups

GET /api/user/(string: email)/groups
Synopsis

Get the list of groups mapped to a user identified by email.

Request Headers
Response Headers
Parameters
  • email – The email of the user.

Response JSON Object
  • user_id (int) – Id of the user.

  • user_email (string) – email of the user.

  • user_group (string) – Group of the user.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/user/admin@SAM.pt/groups":{
      "content":[
         {
            "user_email":"admin@sam.pt",
            "user_group":"Administrators",
            "user_id":1
         },
         {
            "user_email":"admin@sam.pt",
            "user_group":"Users",
            "user_id":1
         }
      ],
      "status":200
   }
}

Check if User is an Admin

GET /api/user/(string: email)/admin
Synopsis

Check if the user identified by email is an administrator.

Request Headers
Response Headers
Parameters
  • email – The email of the user to be checked.

Response JSON Object
  • admin (boolean) – Is the user an admin.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/user/admin@sam.pt/admin":{
      "admin":1,
      "status":200
   }
}

Add User

POST /api/user
Synopsis

Add a new user.

Request Headers
Response Headers
Request JSON Object
  • email (string) – User email.

  • psw (string) – User password.

  • firstname (string) – User first name.

  • lastname (string) – User last name.

  • avatar (string) – avatar of the user (i.e., location in disk).

  • g-recaptcha-response (Object) – Google ReCaptcha response object.

Response JSON Object
  • status (int) – status code.

Status Codes

Important

On a production environment, please, do not post the password without SSL enabled.

Example Request

{
   "email":"new_user@user.com",
   "psw":"123",
   "firstname":"First Name",
   "lastname":"Last Name",
   "avatar":"new_user.png",
   "g-recaptcha-response": null
}

Note

The g-recaptcha-response should not be null, a Google ReCaptcha response should be included in the JSON request object.

Example Response

{
   "/api/user":{
      "status":200
   }
}

Update User

PUT /api/user/(string: email)
Synopsis

Updates a user by email

Request Headers
Response Headers
Parameters
  • email – The email of the user to be updated.

Request JSON Object
  • email (string) – User email.

  • psw (string) – User password.

  • firstname (string) – User first name.

  • lastname (string) – User last name.

  • avatar (string) – avatar of the user (i.e., location in disk).

Response JSON Object
  • status (int) – status code.

Status Codes

Important

On a production environment, please, do not post the password without SSL enabled.

Example Request

{
   "email":"new_user_updated@user.com",
   "firstname":"First Name Updated",
   "lastname":"Last Name Updated",
   "psw":"1234",
   "avatar":"new_user_updated.png",
}

Example Response

{
   "/api/user":{
      "status":200
   }
}

Remove User

DELETE /api/user/(string: email)
Synopsis

Removes a user by email

Request Headers
Response Headers
Parameters
  • email – The email of the user to be removed.

Response JSON Object
  • status (int) – status code.

Status Codes

Example Response

{
   "/api/user/new_user@user.com":{
      "status":200
   }
}

Group Services API

This section includes details concerning services developed for the group entity implemented in
group.py
  1""""""
  2"""
  3// ---------------------------------------------------------------------------
  4//
  5//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
  6//
  7//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
  8//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
  9//
 10//  This program is free software: you can redistribute it and/or modify
 11//  it under the terms of the GNU General Public License as published by
 12//  the Free Software Foundation, either version 3 of the License, or
 13//  (at your option) any later version.
 14//
 15//  This program is distributed in the hope that it will be useful,
 16//  but WITHOUT ANY WARRANTY; without even the implied warranty of
 17//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 18//  GNU General Public License for more details.
 19//
 20//  You should have received a copy of the GNU General Public License
 21//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 22// 
 23//  This work was performed under the scope of Project SECURIoTESIGN with funding 
 24//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
 25//  POCI-01-0145-FEDER-030657) 
 26// ---------------------------------------------------------------------------
 27"""
 28from api import app, mysql
 29from flask import request
 30import modules.error_handlers, modules.utils # SAM's modules
 31import views.user, views.answer # SAM's views
 32
 33"""
 34[Summary]: Adds a new question to the database.
 35[Returns]: Response result.
 36"""
 37@app.route('/api/group', methods=['POST'])
 38def add_group():
 39    DEBUG=True
 40    if request.method != 'POST': return
 41    # Check if the user has permissions to access this resource
 42    views.user.isAuthenticated(request)
 43
 44    json_data = request.get_json()
 45    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
 46    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
 47
 48    # Validate if the necessary data is on the provided JSON 
 49    if (not modules.utils.valid_json(json_data, {"designation"})):
 50        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)   
 51
 52    designation = json_data['designation']
 53    g_modules   = "modules" in json_data and json_data['modules'] or None
 54    g_users     = "users" in json_data and json_data['users'] or None
 55    createdon   = "createdon" in json_data and json_data['createdon'] or None
 56    updatedon   = "updatedon" in json_data and json_data['updatedon'] or None
 57    
 58    # Build the SQL instruction using our handy function to build sql instructions.
 59    values = (designation, createdon, updatedon)
 60    sql, values = modules.utils.build_sql_instruction("INSERT INTO sam.group", ["designation", createdon and "createdon" or None, updatedon and "updatedon" or None], values)
 61    if (DEBUG): print("[SAM-API]: [POST]/api/group - " + sql)
 62
 63    print(g_modules)
 64    print(g_users)
 65    
 66    # Add
 67    n_id = modules.utils.db_execute_update_insert(mysql, sql, values)
 68    if (n_id is None):
 69        return(modules.utils.build_response_json(request.path, 400))  
 70    else:
 71        # If any, link the list of provided users or modules to this group
 72        if (g_users):
 73            for g_user in g_users:
 74                values = (g_user['id'], n_id)
 75                sql, values = modules.utils.build_sql_instruction("INSERT INTO user_group", ["userID", "groupID"], values)
 76                modules.utils.db_execute_update_insert(mysql, sql, values)
 77        if (g_modules):
 78            for g_module in g_modules:
 79                values = (g_module['id'], n_id)
 80                sql, values = modules.utils.build_sql_instruction("INSERT INTO module_group", ["moduleID", "groupID"], values)
 81                modules.utils.db_execute_update_insert(mysql, sql, values)
 82        
 83        return(modules.utils.build_response_json(request.path, 200, {"id": n_id}))  
 84
 85"""
 86[Summary]: Updates a group.
 87[Returns]: Response result.
 88"""
 89@app.route('/api/group', methods=['PUT'])
 90def update_group():
 91    DEBUG=True
 92    if request.method != 'PUT': return
 93    # Check if the user has permissions to access this resource
 94    views.user.isAuthenticated(request)
 95
 96    json_data = request.get_json()
 97    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
 98    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
 99
100    # Validate if the necessary data is on the provided JSON 
101    if (not modules.utils.valid_json(json_data, {"id"})):
102        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
103
104    group_id    = json_data['id']
105    designation = "designation" in json_data and json_data['designation'] or None
106    g_modules   = "modules" in json_data and json_data['modules'] or None
107    g_users     = "users" in json_data and json_data['users'] or None
108    createdon   = "createdon"   in json_data and json_data['createdon'] or None
109    updatedon   = "updatedon"   in json_data and json_data['updatedon'] or None
110
111    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
112    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
113
114    # Build the SQL instruction using our handy function to build sql instructions.
115    values  = (designation, createdon, updatedon)
116    columns = [designation and "designation" or None, createdon and "createdon" or None, updatedon and "updatedOn" or None]
117    where   = "WHERE id="+str(group_id)
118    # Check if there is anything to update (i.e. frontend developer has not sent any values to update).
119    if (len(values) == 0): return(modules.utils.build_response_json(request.path, 200))   
120
121    sql, values = modules.utils.build_sql_instruction("UPDATE sam.group", columns, values, where)
122    if (DEBUG): print("[SAM-API]: [PUT]/api/group - " + sql + " " + str(values))
123
124    # Update Recommendation
125    modules.utils.db_execute_update_insert(mysql, sql, values)
126    
127    # Check if there are any user flagged to be removed, what is removed is not the user but the mapping of a user to the current group.
128    if g_users:
129        for g_user in g_users:
130            flag_remove = "to_remove" in g_user and g_user['to_remove'] or None
131            # Remove the link between the user and the group
132            if (flag_remove):
133                try:
134                    conn    = mysql.connect()
135                    cursor  = conn.cursor()
136                    cursor.execute("DELETE FROM user_group WHERE userID=%s", g_user['id'])
137                    conn.commit()
138                except Exception as e:
139                    print("entrou")
140                    raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
141                finally:
142                    cursor.close()
143                    conn.close()
144            # Add a new user mapping
145            else:
146                values = (g_user['id'], group_id)
147                sql, values = modules.utils.build_sql_instruction("INSERT INTO user_group", ["userID", "groupID"], values)
148                modules.utils.db_execute_update_insert(mysql, sql, values)
149    
150    # Do as above, but for modules.
151    if g_modules:
152        for g_module in g_modules:
153            flag_remove = "to_remove" in g_module and g_module['to_remove'] or None
154            # Remove the link between the user and the group
155            if (flag_remove):
156                try:
157                    conn    = mysql.connect()
158                    cursor  = conn.cursor()
159                    cursor.execute("DELETE FROM module_group WHERE moduleID=%s", g_module['id'])
160                    conn.commit()
161                except Exception as e:
162                    raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
163                finally:
164                    cursor.close()
165                    conn.close()
166            # Add a new module mapping
167            else:
168                values = (g_module['id'], group_id)
169                sql, values = modules.utils.build_sql_instruction("INSERT INTO module_group", ["moduleID", "groupID"], values)
170                modules.utils.db_execute_update_insert(mysql, sql, values)
171
172    return(modules.utils.build_response_json(request.path, 200))   
173
174"""
175[Summary]: Get Groups.
176[Returns]: Response result.
177"""
178@app.route('/api/groups')
179def get_groups():
180    if request.method != 'GET': return
181
182    # 1. Check if the user has permissions to access this resource
183    views.user.isAuthenticated(request)
184
185    # 2. Let's get the answeers for the question from the database.
186    try:
187        conn    = mysql.connect()
188        cursor  = conn.cursor()
189        cursor.execute("SELECT ID, designation, createdon, updatedon FROM sam.group")
190        res = cursor.fetchall()
191    except Exception as e:
192        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
193    
194    # 2.2. Check for empty results 
195    if (len(res) == 0):
196        cursor.close()
197        conn.close()
198        return(modules.utils.build_response_json(request.path, 404))    
199    else:
200        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
201        for row in res:
202            data = {}
203            data['id']          = row[0]
204            data['designation'] = row[1]
205            data['modules']     = find_modules_of_group(row[0])
206            data['users']       = find_users_of_group(row[0])
207            data['createdon']   = row[2]
208            data['updatedon']   = row[3]
209            datas.append(data)
210        cursor.close()
211        conn.close()
212        # 3. 'May the Force be with you, young master'.
213        return(modules.utils.build_response_json(request.path, 200, datas)) 
214
215"""
216[Summary]: Finds the list of modules linked to a type.
217[Returns]: A list of modules or an empty array if None are found.
218"""
219def find_modules_of_group(group_id):
220    try:
221        conn    = mysql.connect()
222        cursor  = conn.cursor()
223        cursor.execute("SELECT moduleID FROM module_group WHERE groupID=%s", group_id)
224        res = cursor.fetchall()
225    except Exception as e:
226        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
227    
228    # Check for empty results 
229    if (len(res) == 0):
230        cursor.close()
231        conn.close()
232        return([]) 
233    else:
234        modules = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
235        for row in res:
236            module = views.module.find_module(row[0], True)[0]
237            del module['tree']
238            del module['dependencies']
239            del module['recommendations']
240            modules.append(module)
241        cursor.close()
242        conn.close()
243       
244        # 'May the Force be with you, young master'.
245        return(modules)
246
247def find_users_of_group(group_id):
248    try:
249        conn    = mysql.connect()
250        cursor  = conn.cursor()
251        cursor.execute("SELECT user_email FROM view_user_group WHERE group_id=%s", group_id)
252        res = cursor.fetchall()
253    except Exception as e:
254        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
255    
256    # Check for empty results 
257    if (len(res) == 0):
258        cursor.close()
259        conn.close()
260        return([]) 
261    else:
262        users = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
263        for row in res:
264            user = views.user.find_user(row[0], True)
265            users.append(user)
266        cursor.close()
267        conn.close()
268       
269        # 'May the Force be with you, young master'.
270        return(users)
271
272"""
273[Summary]: Finds Question.
274[Returns]: Response result.
275"""
276@app.route('/api/group/<ID>', methods=['GET'])
277def find_group(ID):
278    if request.method != 'GET': return
279
280    # 1. Check if the user has permissions to access this resource
281    views.user.isAuthenticated(request)
282
283    # 2. Let's get the answeers for the question from the database.
284    try:
285        conn    = mysql.connect()
286        cursor  = conn.cursor()
287        cursor.execute("SELECT id, designation, createdon, updatedon FROM sam.group WHERE id=%s", ID)
288        res = cursor.fetchall()
289    except Exception as e:
290        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
291    
292    # 2.2. Check for empty results 
293    if (len(res) == 0):
294        cursor.close()
295        conn.close()
296        return(modules.utils.build_response_json(request.path, 404))    
297    else:
298        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
299        for row in res:
300            data = {}
301            data['id']          = row[0]
302            data['designation'] = row[1]
303            data['modules']     = find_modules_of_group(row[0])
304            data['users']       = find_users_of_group(row[0])
305            data['createdon']   = row[2]
306            data['updatedon']   = row[3]
307            datas.append(data)
308        cursor.close()
309        conn.close()
310        # 3. 'May the Force be with you, young master'.
311        return(modules.utils.build_response_json(request.path, 200, datas)) 
312
313"""
314[Summary]: Delete a Group.
315[Returns]: Returns a success or error response
316"""
317@app.route('/api/group/<ID>', methods=["DELETE"])
318def delete_group(ID):
319    if request.method != 'DELETE': return
320    # 1. Check if the user has permissions to access this resource.
321    views.user.isAuthenticated(request)
322
323    # 2. Connect to the database and delete the resource.
324    try:
325        conn    = mysql.connect()
326        cursor  = conn.cursor()
327        cursor.execute("DELETE FROM sam.group WHERE ID=%s", ID)
328        conn.commit()
329    except Exception as e:
330        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
331    finally:
332        cursor.close()
333        conn.close()
334
335    # 3. The Delete request was a success, the user 'took the blue pill'.
336    return (modules.utils.build_response_json(request.path, 200))

Get Groups

GET /api/group
Synopsis

Get all groups.

Request Headers
Response Headers
Response JSON Object
  • id (int) – Id of the group.

  • designation (string) – Description of the group.

  • modules (array) – Array of modules mapped to the group.

  • users (array) – Array of users.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/groups":{
      "content":[
         {
            "createdon":"Thu, 18 Nov 2021 12:38:43 GMT",
            "designation":"Administrators",
            "id":1,
            "modules":[],
            "updatedon":"Thu, 18 Nov 2021 12:38:43 GMT",
            "users":[
               {
                  "avatar":null,
                  "email":"admin@sam.pt",
                  "firstName":"Administrator",
                  "id":1,
                  "lastName":null
               }
            ]
         }
      ],
      "status":200
   }
}

GET /api/group/(int: id)
Synopsis

Get a group by id.

Request Headers
Response Headers
Parameters
  • id – The id of the group to be retrieved.

Response JSON Object
  • id (int) – Id of the group.

  • designation (string) – Description of the group.

  • modules (array) – Array of modules mapped to the group.

  • users (array) – Array of users.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/groups":{
      "content":[
         {
            "createdon":"Thu, 18 Nov 2021 12:38:43 GMT",
            "designation":"Administrators",
            "id":1,
            "modules":[],
            "updatedon":"Thu, 18 Nov 2021 12:38:43 GMT",
            "users":[
               {
                  "avatar":null,
                  "email":"admin@sam.pt",
                  "firstName":"Administrator",
                  "id":1,
                  "lastName":null
               }
            ]
         }
      ],
      "status":200
   }
}

Add Group

POST /api/group
Synopsis

Adds a new group.

Request Headers
Response Headers
Request JSON Object
  • designation (string) – The name of the group.

  • users (array) – The id of each user that will belong to the new group.

  • modules (array) – The id of each module that will belong to the new group.

Response JSON Object
  • id (int) – Id assigned to the new group.

  • status (int) – status code.

Status Codes

Example Request

{"designation":"New Group","users":[{"id":1}],"modules":[{"id":1}]}

Example Response

{"/api/group":{"id":3,"status":200}}

Edit Group

PUT /api/group
Synopsis

Updates the information of a group.

Request Headers
Response Headers
Request JSON Object
  • id (int) – The id of the group you want to update.

  • designation (string) – The new name of the group.

Response JSON Object
  • status (int) – status code.

Status Codes

Example Request

{"id":3,"designation":"New Group Updated"}

Example Response

{"/api/group":{"status":200}}

Remove Group

DELETE /api/group/(int: id)
Synopsis

Remove a group by id.

Request Headers
Response Headers
Parameters
  • id – The id of the group to be removed.

Response JSON Object
  • status (int) – status code.

Status Codes

Example Response

{"/api/group/3":{"status":200}}

File Services API

This section includes details concerning services developed for the file entity implemented in
file.py.
 1"""
 2// ---------------------------------------------------------------------------
 3//
 4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
 5//
 6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
 7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
 8//
 9//  This program is free software: you can redistribute it and/or modify
10//  it under the terms of the GNU General Public License as published by
11//  the Free Software Foundation, either version 3 of the License, or
12//  (at your option) any later version.
13//
14//  This program is distributed in the hope that it will be useful,
15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17//  GNU General Public License for more details.
18//
19//  You should have received a copy of the GNU General Public License
20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
21// 
22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
24//  POCI-01-0145-FEDER-030657) 
25// ---------------------------------------------------------------------------
26"""
27from api import app
28from flask import request, abort, jsonify, send_from_directory
29import os, views.user, modules.utils
30from werkzeug.utils import secure_filename
31
32UPLOAD_DIRECTORY="./external/"
33
34@app.route("/api/file/<path:path>", methods=['GET'])
35def get_file(path):
36    if request.method != 'GET': return
37    # Check if the user has permissions to access this resource
38    views.user.isAuthenticated(request)
39    
40    return send_from_directory(UPLOAD_DIRECTORY, path, as_attachment=True)
41
42@app.route("/api/files", methods=['GET'])
43def list_files():
44    if request.method != 'GET': return
45    # Check if the user has permissions to access this resource
46    views.user.isAuthenticated(request)
47
48    files = []
49    for filename in os.listdir(UPLOAD_DIRECTORY):
50        path = os.path.join(UPLOAD_DIRECTORY, filename)
51        if os.path.isfile(path):
52            files.append(filename)
53    return jsonify(files)
54
55@app.route("/api/file/<filename>", methods=["POST"])
56def post_file(filename):
57    if request.method != 'POST': return
58    file = request.files['file']
59    # Check if the user has permissions to access this resource
60    views.user.isAuthenticated(request)
61
62    if "/" in filename:
63        # Return 400 BAD REQUEST
64        abort(400, "no subdirectories directories allowed")
65
66    if file:
67        filename = secure_filename(filename)
68        file.save(UPLOAD_DIRECTORY + filename)
69
70    # Return 201 CREATED
71    return(modules.utils.build_response_json(request.path, 201))
72
73@app.route('/api/file/module/<module_name>/session/<ID>', methods=['GET'])
74def download_recommendations_zip(module_name, ID):
75    if request.method != 'GET': return
76    # Check if the user has permissions to access this resource
77    views.user.isAuthenticated(request)
78    
79    file_name = str(module_name)+'_session_'+str(ID)+'.zip'
80    return send_from_directory('./temp/', file_name, as_attachment=True)

Upload File

POST /api/file/(string: filename)
Synopsis

Uploads a file to the predefined UPLOAD_DIRECTORY.

Request Headers
Response Headers
Parameters
  • filename – The name of the file.

Form Parameters
  • file – The file to be uploaded.

Response JSON Object
  • status (int) – status code.

Status Codes

Example Response

{
   "/api/file/example.md":{
      "status":201
   }
}

Request File

GET /api/file/(string: filename)
Synopsis

Returns a file from the predefined UPLOAD_DIRECTORY.

Request Headers
Response Headers
Parameters
  • filename – The name of the file.

Status Codes

Get Files

GET /api/file/(string: filename)
Synopsis

Returns the list of files available on the predefined UPLOAD_DIRECTORY.

Request Headers
Response Headers
Parameters
  • filename – The name of the file.

Status Codes
  • 200 OK – The list of files was successfully retrieved.

Example Response

["example.md","example_2.md"]

Get Session Files

GET /api/file/module/(string: shortname)/session/(int: id)
Synopsis

Returns a zip containing all recommendations for a specific session id and module identified by shortname.

Request Headers
Response Headers
Parameters
  • shortname – The short name of the module.

  • id – The id of the session.

Status Codes
  • 200 OK – The zip file was successfully created.

Types Services API

This section includes details concerning services developed for the type entity implemented in
type.py.
  1"""
  2// ---------------------------------------------------------------------------
  3//
  4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
  5//
  6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
  7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
  8//
  9//  This program is free software: you can redistribute it and/or modify
 10//  it under the terms of the GNU General Public License as published by
 11//  the Free Software Foundation, either version 3 of the License, or
 12//  (at your option) any later version.
 13//
 14//  This program is distributed in the hope that it will be useful,
 15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
 16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17//  GNU General Public License for more details.
 18//
 19//  You should have received a copy of the GNU General Public License
 20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 21// 
 22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
 23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
 24//  POCI-01-0145-FEDER-030657) 
 25// ---------------------------------------------------------------------------
 26"""
 27from api import app, mysql
 28from flask import request
 29import modules.error_handlers, modules.utils # SAM's modules
 30import views.user # SAM's views
 31
 32"""
 33[Summary]: Adds a new type to the database.
 34[Returns]: Response result.
 35"""
 36@app.route('/api/type', methods=['POST'])
 37def add_type():
 38    DEBUG=True
 39    if request.method != 'POST': return
 40    # Check if the user has permissions to access this resource
 41    views.user.isAuthenticated(request)
 42
 43    json_data = request.get_json()
 44    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
 45    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
 46
 47    # Validate if the necessary data is on the provided JSON 
 48    if (not modules.utils.valid_json(json_data, {"name"})):
 49        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
 50
 51    name        = json_data['name']
 52    description = "description" in json_data and json_data['description'] or None
 53    createdon   = "createdon" in json_data and json_data['createdon'] or None
 54    updatedon   = "updatedon" in json_data and json_data['updatedon'] or None
 55    
 56    # Build the SQL instruction using our handy function to build sql instructions.
 57    values = (name, description, createdon, updatedon)
 58    sql, values = modules.utils.build_sql_instruction("INSERT INTO type", ["name", description and "description" or None, createdon and "createdon" or None, updatedon and "updatedon" or None], values)
 59    if (DEBUG): print("[SAM-API]: [POST]/api/type - " + sql)
 60
 61    # Add
 62    n_id = modules.utils.db_execute_update_insert(mysql, sql, values)
 63    if (n_id is None):
 64        return(modules.utils.build_response_json(request.path, 400))  
 65    else:
 66        return(modules.utils.build_response_json(request.path, 200, {"id": n_id}))
 67
 68"""
 69[Summary]: Updates a type.
 70[Returns]: Response result.
 71"""
 72@app.route('/api/type', methods=['PUT'])
 73def update_type():
 74    DEBUG=True
 75    if request.method != 'PUT': return
 76    # Check if the user has permissions to access this resource
 77    views.user.isAuthenticated(request)
 78
 79    json_data = request.get_json()
 80    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
 81    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
 82
 83    # Validate if the necessary data is on the provided JSON 
 84    if (not modules.utils.valid_json(json_data, {"id"})):
 85        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
 86
 87    name        = "name"        in json_data and json_data['name'] or None
 88    description = "description" in json_data and json_data['description'] or None
 89    createdon   = "createdon"   in json_data and json_data['createdon'] or None
 90    updatedon   = "updatedon"   in json_data and json_data['updatedon'] or None
 91
 92    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
 93    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
 94
 95    # Build the SQL instruction using our handy function to build sql instructions.
 96    values  = (name, description, createdon, updatedon)
 97    columns = [name and "name" or None, description and "description" or None, createdon and "createdon" or None, updatedon and "updatedOn" or None]
 98    where   = "WHERE id="+str(json_data['id'])
 99    # Check if there is anything to update (i.e. frontend developer has not sent any values to update).
100    if (len(values) == 0): return(modules.utils.build_response_json(request.path, 200))   
101
102    sql, values = modules.utils.build_sql_instruction("UPDATE type", columns, values, where)
103    if (DEBUG): print("[SAM-API]: [PUT]/api/type - " + sql + " " + str(values))
104
105    # Update Recommendation
106    modules.utils.db_execute_update_insert(mysql, sql, values)
107
108    return(modules.utils.build_response_json(request.path, 200))   
109
110"""
111[Summary]: Get Questions.
112[Returns]: Response result.
113"""
114@app.route('/api/types')
115def get_types():
116    if request.method != 'GET': return
117
118    # 1. Check if the user has permissions to access this resource
119    views.user.isAuthenticated(request)
120
121    # 2. Let's get the answeers for the question from the database.
122    try:
123        conn    = mysql.connect()
124        cursor  = conn.cursor()
125        cursor.execute("SELECT ID, name, description, createdOn, updatedOn FROM type")
126        res = cursor.fetchall()
127    except Exception as e:
128        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
129    
130    # 2.2. Check for empty results 
131    if (len(res) == 0):
132        cursor.close()
133        conn.close()
134        return(modules.utils.build_response_json(request.path, 404))    
135    else:
136        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
137        for row in res:
138            data = {}
139            data['id']          = row[0]
140            data['name']        = row[1]
141            data['description'] = row[2]
142            data['modules']     = find_modules_of_type(row[0])
143            data['createdon']   = row[3]
144            data['updatedon']   = row[4]
145            datas.append(data)
146        cursor.close()
147        conn.close()
148        # 3. 'May the Force be with you, young master'.
149        return(modules.utils.build_response_json(request.path, 200, datas)) 
150
151"""
152[Summary]: Finds the list of modules linked to a type.
153[Returns]: A list of modules or an empty array if None are found.
154"""
155def find_modules_of_type(type_id):
156    try:
157        conn    = mysql.connect()
158        cursor  = conn.cursor()
159        cursor.execute("SELECT ID FROM module WHERE typeID=%s", type_id)
160        res = cursor.fetchall()
161    except Exception as e:
162        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
163    
164    # Check for empty results 
165    if (len(res) == 0):
166        cursor.close()
167        conn.close()
168        return([]) 
169    else:
170        modules = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
171        for row in res:
172            module = views.module.find_module(row[0], True)[0]
173            del module['tree']
174            del module['dependencies']
175            del module['recommendations']
176            modules.append(module)
177        cursor.close()
178        conn.close()
179       
180        # 'May the Force be with you, young master'.
181        return(modules)
182
183"""
184[Summary]: Finds a Type.
185[Returns]: Response result.
186"""
187@app.route('/api/type/<ID>', methods=['GET'])
188def find_type(ID):
189    if request.method != 'GET': return
190
191    # 1. Check if the user has permissions to access this resource
192    views.user.isAuthenticated(request)
193
194    # 2. Let's get the answeers for the question from the database.
195    try:
196        conn    = mysql.connect()
197        cursor  = conn.cursor()
198        cursor.execute("SELECT ID, name, description, createdOn, updatedOn FROM type WHERE ID=%s", ID)
199        res = cursor.fetchall()
200    except Exception as e:
201        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
202    
203    # 2.2. Check for empty results 
204    if (len(res) == 0):
205        cursor.close()
206        conn.close()
207        return(modules.utils.build_response_json(request.path, 404))    
208    else:
209        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
210        for row in res:
211            data = {}
212            data['id']          = row[0]
213            data['name']        = row[1]
214            data['description'] = row[2]
215            data['createdon']   = row[3]
216            data['updatedon']   = row[4]
217            datas.append(data)
218        cursor.close()
219        conn.close()
220        # 3. 'May the Force be with you, young master'.
221        return(modules.utils.build_response_json(request.path, 200, datas)) 
222
223"""
224[Summary]: Delete a type.
225[Returns]: Returns a success or error response
226"""
227@app.route('/api/type/<ID>', methods=["DELETE"])
228def delete_type(ID):
229    if request.method != 'DELETE': return
230    # 1. Check if the user has permissions to access this resource.
231    views.user.isAuthenticated(request)
232
233    # 2. Connect to the database and delete the resource.
234    try:
235        conn    = mysql.connect()
236        cursor  = conn.cursor()
237        cursor.execute("DELETE FROM type WHERE ID=%s", ID)
238        conn.commit()
239    except Exception as e:
240        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
241    finally:
242        cursor.close()
243        conn.close()
244
245    # 3. The Delete request was a success, the user 'took the blue pill'.
246    return (modules.utils.build_response_json(request.path, 200))

Get Types

GET /api/types
Synopsis

Get types.

Request Headers
Response Headers
Response JSON Object
  • id (int) – Id of the type.

  • name (string) – Name of the type.

  • description (string) – Description of the type.

  • modules (array) – Array of modules mapped to the current type.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/types":{
      "content":[
         {
            "id":1,
            "name":"Test Type",
            "description":"Test Type Description",
            "modules":[],
            "createdon":"Mon, 20 Sep 2021 09:00:00 GMT",
            "updatedon":"Mon, 20 Sep 2021 09:00:00 GMT"
         }
      ],
      "status":200
   }
}

GET /api/type/(int: id)
Synopsis

Get a type identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – id of the type.

Response JSON Object
  • id (int) – Id of the type.

  • name (string) – Name of the type.

  • description (string) – Description of the type.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/type/1":{
      "content":[
         {
            "id":1,
            "name":"Test Type",
            "description":"Test Type Description",
            "createdon":"Mon, 20 Sep 2021 09:00:00 GMT",
            "updatedon":"Mon, 20 Sep 2021 09:00:00 GMT"
         }
      ],
      "status":200
   }
}

Add Type

POST /api/type
Synopsis

Add a new type.

Request Headers
Response Headers
Request JSON Object
  • name (string) – Name of the type.

  • description (string) – Description of the type.

Response JSON Object
  • status (int) – status code.

Status Codes

Example Request

{
   "name":"Test Type",
   "description":"Test Type Description"
}

Example Response

{"/api/type":{"status":200}}

Edit Type

PUT /api/type
Synopsis

Update a type.

Request Headers
Response Headers
Request JSON Object
  • id (int) – Id of the module to update.

  • name (string) – Name of the type.

  • description (string) – Description of the type.

Response JSON Object
  • status (int) – status code.

Status Codes

Example Request

{
   "id":1,
   "name":"Test Type Updated",
   "description":"Test Type Updated"
}

Example Response

{"/api/type":{"status":200}}

Remove Type

DELETE /api/type/(int: id)
Synopsis

Remove a type identified by id.

Request Headers
Response Headers
Parameters
  • id – The id of the type to be removed.

Response JSON Object
  • status (int) – status code.

Status Codes

Example Response

{"/api/type/1":{"status":200}}

Module Services API

This section includes details concerning services developed for the module entity implemented in
module.py.
   1"""
   2// ---------------------------------------------------------------------------
   3//
   4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
   5//
   6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
   7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
   8//
   9//  This program is free software: you can redistribute it and/or modify
  10//  it under the terms of the GNU General Public License as published by
  11//  the Free Software Foundation, either version 3 of the License, or
  12//  (at your option) any later version.
  13//
  14//  This program is distributed in the hope that it will be useful,
  15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
  16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  17//  GNU General Public License for more details.
  18//
  19//  You should have received a copy of the GNU General Public License
  20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
  21// 
  22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
  23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
  24//  POCI-01-0145-FEDER-030657) 
  25// ---------------------------------------------------------------------------
  26"""
  27from api import app, mysql
  28from flask import request
  29from datetime import datetime
  30import os
  31import modules.error_handlers, modules.utils # SAM's modules
  32import views.user, views.recommendation, views.question, views.dependency # SAM's views
  33
  34"""
  35[Summary]: Adds a new Module.
  36[Returns]: Response result.
  37"""
  38@app.route('/api/module', methods=['POST'])
  39def add_module():
  40    DEBUG=False
  41    if request.method != 'POST': return
  42    # Check if the user has permissions to access this resource
  43    views.user.isAuthenticatedAdmin(request)
  44
  45    json_data = request.get_json()
  46    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
  47    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
  48
  49    # Validate if the necessary data is on the provided JSON
  50    if (not modules.utils.valid_json(json_data, {"shortname", "fullname", "displayname"})):
  51        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
  52    
  53    shortname       = json_data['shortname']
  54    fullname        = json_data['fullname']
  55    displayname     = json_data['displayname']
  56
  57    # Check if module's short name and display name are unique
  58    shortnames, displaynames = get_modules_short_displaynames()
  59    if shortname in shortnames:
  60        raise modules.error_handlers.BadRequest(request.path, str("'Abbreviation' already in use."), 500) 
  61    if displayname in displaynames:
  62        raise modules.error_handlers.BadRequest(request.path, str("'Display Name' already in use."), 500) 
  63
  64    tree            = None
  65    if ('tree' in json_data): 
  66        tree = json_data['tree']
  67    recommendations = "recommendations" in json_data and json_data['recommendations'] or None
  68    dependencies    = "dependencies" in json_data and json_data['dependencies'] or None
  69    avatar      = "avatar" in json_data and json_data['avatar'] or None
  70    description = "description" in json_data and json_data['description'] or None
  71    type_id     = "type_id" in json_data and json_data['type_id'] or None
  72    logic       = "logic_filename" in json_data and json_data['logic_filename'] or None
  73    createdon   = "createdon" in json_data and json_data['createdon'] or None
  74    updatedon   = "updatedon" in json_data and json_data['updatedon'] or None
  75    
  76    # Build the SQL instruction using our handy function to build sql instructions.
  77    columns = ["shortname", "fullname", "displayname", type_id and "typeID" or None, avatar and "avatar" or None, description and "description" or None, createdon and "createdon" or None, updatedon and "updatedon" or None]
  78    values  = (shortname, fullname, displayname, type_id, avatar, description, createdon, updatedon)
  79    
  80    sql, values = modules.utils.build_sql_instruction("INSERT INTO module", columns, values)
  81    if (DEBUG): print("[SAM-API]: [POST]/api/module - " + sql + " => " + str(values))
  82
  83    # Add Module and iterate the tree of the module in order to create the questions and answers mapped to the current module.
  84    module_id = modules.utils.db_execute_update_insert(mysql, sql, values)
  85    if ('tree' in json_data):
  86        for node in tree: 
  87            iterate_tree_nodes(recommendations, "INSERT", module_id, node)
  88    
  89    # Store the mapping of question_answer and recommendations (DB table Recommendation_Question_Answer)
  90    # Get the question_answer id primary key value, through [question_id] and [answer_id]
  91    if recommendations:
  92        for recommendation in recommendations:
  93            for question_answer in recommendation['questions_answers']:
  94                qa_res = views.question.find_question_answers_2(question_answer['question_id'], question_answer['answer_id'], True)
  95                if (qa_res is None): return(modules.utils.build_response_json(request.path, 400)) 
  96                qa_res = qa_res[0]
  97                if (DEBUG): print("[SAM-API] [POST]/api/module - question_id = " + str(question_answer['question_id']) + ", answer_id=" + str(question_answer['answer_id']) + " => Question_Answer_id =" + str(qa_res['question_answer_id']))
  98                question_answer['id'] = qa_res['question_answer_id']
  99    
 100        # Add the recommendation with the link between questions and answers
 101        for recommendation in recommendations:
 102            views.recommendation.add_recommendation(recommendation)
 103    
 104    # Add dependencies, only if the current module depends on another module.
 105    if (module_id and dependencies):
 106        for dependency in dependencies:
 107                views.dependency.add_dependency({"module_id": module_id, "depends_on": dependency['module']['id']}, True)
 108
 109    # If availabe, set the logic filename after knowing the database id of the module
 110    if logic and module_id:
 111        final_logic_filename = "logic_" + str(module_id) + ".py"
 112        sql, values = modules.utils.build_sql_instruction("UPDATE module", ["logicFilename"], final_logic_filename, "WHERE id="+str(module_id))
 113        modules.utils.db_execute_update_insert(mysql, sql, values, True)
 114
 115    # 'Do, or do not, there is no try.'
 116    return(modules.utils.build_response_json(request.path, 200, {"id": module_id, "tree": tree}))  
 117
 118"""
 119[Summary]: Delete logic file linked to a module.
 120[Returns]: Returns a success or error response
 121"""
 122@app.route('/api/module/<ID>/logic', methods=["DELETE"])
 123def delete_module_logic(ID, internal_call=False):
 124    if (not internal_call):
 125        if request.method != 'DELETE': return
 126    
 127    # Check if the user has permissions to access this resource
 128    if (not internal_call): views.user.isAuthenticatedAdmin(request)
 129
 130    # Get information about module and remove logic file
 131    module = find_module(ID, True)
 132
 133    if (module[0]['logic_filename']):
 134        try:
 135            os.remove(os.getcwd() + "/external/" + module[0]['logic_filename'])
 136        except OSError as e:
 137            raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 138
 139    # The Delete request was a success, the user 'took the blue pill'.
 140    if (not internal_call):
 141        return (modules.utils.build_response_json(request.path, 200))
 142    else:
 143        return(True)
 144
 145
 146"""
 147[Summary]: Delete questions linked to a module.
 148[Returns]: Returns a success or error response
 149"""
 150@app.route('/api/module/<ID>/questions', methods=["DELETE"])
 151def delete_module_questions(ID, internal_call=False):
 152    if (not internal_call):
 153        if request.method != 'DELETE': return
 154    
 155    # Check if the user has permissions to access this resource
 156    if (not internal_call): views.user.isAuthenticatedAdmin(request)
 157
 158    # Get the set of questions linked to this module.
 159    questions = find_module_questions(ID, True)
 160    if (questions):
 161        for question in questions:
 162            views.question.delete_question(question['id'], True)
 163    
 164    # Delete the module itself and all the sessions linked to him.
 165    # delete_module(ID, True)
 166
 167    # The Delete request was a success, the user 'took the blue pill'.
 168    if (not internal_call):
 169        return (modules.utils.build_response_json(request.path, 200))
 170    else:
 171        return(True)
 172
 173"""
 174[Summary]: Delete answers linked to a module.
 175[Returns]: Returns a success or error response
 176"""
 177@app.route('/api/module/<ID>/answers', methods=["DELETE"])
 178def delete_module_answers(ID, internal_call=False):
 179    if (not internal_call): 
 180        if request.method != 'DELETE': return
 181
 182    # Check if the user has permissions to access this resource
 183    if (not internal_call): views.user.isAuthenticatedAdmin(request)
 184
 185    # Get the set of answers linked to this module.
 186        # Get the set of questions linked to this module.
 187    answers = find_module_answers(ID, True)
 188    if (answers):
 189        for answer in answers:
 190            views.answer.delete_answer(answer['id'], True)
 191    
 192    # Delete the module itself and all the sessions linked to him.
 193    # delete_module(ID, True)
 194    
 195    if (not internal_call):
 196        return (modules.utils.build_response_json(request.path, 200))
 197    else:
 198        return(True)
 199
 200
 201"""
 202[Summary]: Delete a module (partial delete - Linked questions and answers are not deleted)
 203[Returns]: Returns a success or error response
 204"""
 205@app.route('/api/module/<ID>', methods=["DELETE"])
 206def delete_module_partial(ID, internal_call=False):
 207    if (not internal_call):
 208        if request.method != 'DELETE': return
 209    
 210    # 1. Check if the user has permissions to access this resource
 211    if (not internal_call):
 212        views.user.isAuthenticatedAdmin(request)
 213
 214    # 2. Delete logic file associated
 215    delete_module_logic(ID, True)
 216
 217    # 3. Connect to the database and delete the resource
 218    try:
 219        conn    = mysql.connect()
 220        cursor  = conn.cursor()
 221        cursor.execute("DELETE FROM module WHERE ID=%s", ID)
 222        conn.commit()
 223    except Exception as e:
 224        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
 225    finally:
 226        cursor.close()
 227        conn.close()
 228
 229    # 4. The Delete request was a success, the user 'took the blue pill'.
 230    if (not internal_call): 
 231        return (modules.utils.build_response_json(request.path, 200))
 232    else:
 233        return(True)
 234
 235"""
 236[Summary]: Fully delete a module (including sessions, linked questions and answers)
 237[Returns]: Returns a success or error response
 238"""
 239@app.route('/api/module/<ID>/full', methods=["DELETE"])
 240def delete_module_full(ID, internal_call=False):
 241    if (not internal_call): 
 242        if request.method != 'DELETE': return
 243
 244    # Check if the user has permissions to access this resource
 245    if (not internal_call): views.user.isAuthenticatedAdmin(request)
 246
 247    # Get and delete the set of answers and questions linked to this module.
 248    delete_module_answers(ID, True)
 249    delete_module_questions(ID, True)
 250    
 251    # Delete the module itself and all the sessions linked to him.
 252    delete_module_partial(ID, True)
 253    
 254    if (not internal_call):
 255        return (modules.utils.build_response_json(request.path, 200))
 256    else:
 257        return(True)
 258
 259"""
 260[Summary]: Updates a Module.
 261[Returns]: returns 200 if the operation was a success, 500 otherwise.
 262"""
 263@app.route('/api/module', methods=['PUT'])
 264def update_module():
 265    DEBUG=False
 266    # Check if the user has permissions to access this resource
 267    views.user.isAuthenticatedAdmin(request)
 268    # Delete the module but not to forget to preserve the session related to this module that  is being delete just for the sake of being easier to update is info based on the tree parsed
 269    if request.method != 'PUT': return
 270    json_data = request.get_json()
 271    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
 272    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
 273
 274    # Validate if the necessary data is on the provided JSON 
 275    if (not modules.utils.valid_json(json_data, {"id"})):
 276        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
 277
 278    module_id       = json_data['id']
 279    tree            = None
 280    # If there is a tree to update
 281    if ('tree' in json_data):
 282        modules.utils.console_log("[PUT]/api/module", "Tree exists")
 283        tree        = json_data['tree']
 284    # If the user has choosen to erase all questions
 285    if (tree is None):
 286        modules.utils.console_log("[PUT]/api/module", "No tree exists")
 287        sql     = "DELETE FROM module_question WHERE moduleID=%s"
 288        values  = module_id
 289        modules.utils.db_execute_update_insert(mysql, sql, values)
 290
 291    #
 292    dependencies    = "dependencies" in json_data and json_data['dependencies'] or None
 293    recommendations = "recommendations" in json_data and json_data['recommendations'] or None
 294    shortname       = "shortname" in json_data and json_data['shortname'] or None
 295    fullname        = "fullname" in json_data and json_data['fullname'] or None
 296    displayname     = "displayname" in json_data and json_data['displayname'] or None
 297    avatar          = "avatar" in json_data and json_data['avatar'] or None
 298    description     = "description" in json_data and json_data['description'] or None
 299    type_id         = "type_id" in json_data and json_data['type_id'] or None
 300    logic           = "logic_filename" in json_data and "logic_"+str(module_id)+".py" or None
 301    createdon       = None
 302    updatedon       = datetime.now()
 303    
 304    # Build the SQL instruction using our handy function to build sql instructions.
 305    columns = [shortname and "shortname" or None, fullname and "fullname" or None, displayname and "displayname" or None, type_id and "typeID" or None, logic and "logicFilename" or None, avatar and "avatar" or None, description and "description" or None, createdon and "createdon" or None, updatedon and "updatedon" or None]
 306    values  = (shortname, fullname, displayname, type_id, logic, avatar, description, createdon, updatedon)
 307    where   = "WHERE id="+str(module_id)
 308
 309    sql, values = modules.utils.build_sql_instruction("UPDATE module", columns, values, where)
 310    if (DEBUG): modules.utils.console_log("[PUT]/api/module", str(sql + " => " + str(values) + " " + where))
 311
 312    # Update 
 313    modules.utils.db_execute_update_insert(mysql, sql, values)
 314    
 315    # Iterate the tree of module in order to update questions an answers of the module
 316    if ('tree' in json_data):
 317        for node in tree:   
 318            iterate_tree_nodes(recommendations, "UPDATE", module_id, node)
 319
 320    # Update the dependency
 321    if (dependencies):
 322        for dependency in dependencies:
 323            flag_remove = "to_remove" in dependency and dependency['to_remove'] or None
 324            # Remove the dependency
 325            if (flag_remove):
 326                views.dependency.delete_dependency(dependency['id'], True)
 327            else:
 328                views.dependency.add_dependency({"module_id": module_id, "depends_on": dependency['module']['id']}, True)
 329
 330    if (recommendations):
 331        # Check if there are any recommendation flagged to be removed, what is removed is not the recommentation but the mapping of a recommendation to the current module
 332        for recommendation in recommendations:
 333            flag_remove = "to_remove" in recommendation and recommendation['to_remove'] or None
 334            # Remove the recommendation
 335            if (flag_remove):
 336                views.recommendation.remove_recommendation_of_module(recommendation['id'], module_id, True)
 337            # Add a new recommendation
 338            else:
 339                # Store the mapping of question_answer and recommendations (DB table Recommendation_Question_Answer)
 340                # Get the question_answer id primary key value, through [question_id] and [answer_id]    
 341                for question_answer in recommendation['questions_answers']:
 342                    qa_res = views.question.find_question_answers_2(question_answer['question_id'], question_answer['answer_id'], True)
 343                    if (qa_res is None): return(modules.utils.build_response_json(request.path, 400)) 
 344                    qa_res = qa_res[0]
 345                    if (DEBUG): print("[SAM-API] [POST]/api/module - question_id = " + str(question_answer['question_id']) + ", answer_id=" + str(question_answer['answer_id']) + " => Question_Answer_id =" + str(qa_res['question_answer_id']))
 346                    question_answer['id'] = qa_res['question_answer_id']
 347                    print("!---->" + str(question_answer['id']))
 348                
 349                # Add the recommendation with the link between questions and answers
 350                views.recommendation.add_recommendation(recommendation)
 351    
 352    return(modules.utils.build_response_json(request.path, 200, {"id": module_id}))  
 353
 354"""
 355[Summary]: Get modules.
 356[Returns]: Returns a set of modules.
 357"""
 358@app.route('/api/modules', methods=['GET'])
 359def get_modules():
 360    if request.method != 'GET': return
 361
 362    # 1. Check if the user has permissions to access this resource
 363    views.user.isAuthenticated(request)
 364
 365    # 2. Let's get the modules from the database.
 366    try:
 367        conn    = mysql.connect()
 368        cursor  = conn.cursor()
 369        cursor.execute("SELECT ID, typeID, shortname, fullname, displayname, logicfilename, description, avatar, createdon, updatedon FROM module WHERE disable = 0")
 370        res = cursor.fetchall()
 371    except Exception as e:
 372        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 373    
 374    # 2.1. Check for empty results. 
 375    if (len(res) == 0):
 376        cursor.close()
 377        conn.close()
 378        return(modules.utils.build_response_json(request.path, 404))    
 379    else:
 380        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
 381        for row in res:
 382            data = {}
 383            data['id']              = row[0]
 384            data['type_id']         = row[1]
 385            data['shortname']       = row[2]
 386            data['fullname']        = row[3]
 387            data['displayname']     = row[4]
 388            data['logic_filename']  = row[5]
 389            data['plugin']          = check_plugin(row[0], True)
 390            data['description']     = row[6]
 391            data['avatar']          = row[7]
 392            data['createdon']       = row[8]
 393            data['updatedon']       = row[9]
 394            datas.append(data)
 395
 396        cursor.close()
 397        conn.close()
 398        # 3. 'May the Force be with you, young padawan'.
 399        return(modules.utils.build_response_json(request.path, 200, datas))    
 400
 401"""
 402[Summary]: Get questions of each module.
 403[Returns]: Returns a set of modules.
 404"""
 405@app.route('/api/modules/questions', methods=['GET'])
 406def get_modules_questions():
 407    if request.method != 'GET': return
 408
 409    # Check if the user has permissions to access this resource
 410    views.user.isAuthenticated(request)
 411
 412    # Let's get the resource from the DB
 413    try:
 414        conn    = mysql.connect()
 415        cursor  = conn.cursor()
 416        cursor.execute("SELECT DISTINCT module_id FROM view_module_questions_answers")
 417        res = cursor.fetchall()
 418    except Exception as e:
 419        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 420    
 421    # Check for empty results. 
 422    if (len(res) == 0):
 423        cursor.close()
 424        conn.close()
 425        return(modules.utils.build_response_json(request.path, 404))    
 426    else:
 427        datas = []
 428        # Module IDs first
 429        for row in res:
 430            module = {}
 431            module['id']            = row[0]
 432            module['questions']     = find_module_questions(module['id'], True)
 433            datas.append(module)
 434    cursor.close()
 435    conn.close()
 436    
 437    # 'May the Force be with you, young padawan'.
 438    return(modules.utils.build_response_json(request.path, 200, datas))    
 439
 440"""
 441[Summary]: Get answers of each module.
 442[Returns]: Returns a set of modules.
 443"""
 444@app.route('/api/modules/answers', methods=['GET'])
 445def get_modules_answers():
 446    if request.method != 'GET': return
 447
 448    # Check if the user has permissions to access this resource
 449    views.user.isAuthenticated(request)
 450
 451    # Let's get the resource from the DB
 452    try:
 453        conn    = mysql.connect()
 454        cursor  = conn.cursor()
 455        cursor.execute("SELECT DISTINCT module_id FROM view_module_questions_answers")
 456        res = cursor.fetchall()
 457    except Exception as e:
 458        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 459    
 460    # Check for empty results. 
 461    if (len(res) == 0):
 462        cursor.close()
 463        conn.close()
 464        return(modules.utils.build_response_json(request.path, 404))    
 465    else:
 466        datas = []
 467        # Module IDs first
 468        for row in res:
 469            module = {}
 470            module['id']            = row[0]
 471            module['answers']     = find_module_answers(module['id'], True)
 472            datas.append(module)
 473    cursor.close()
 474    conn.close()
 475    
 476    # 'May the Force be with you, young padawan'.
 477    return(modules.utils.build_response_json(request.path, 200, datas))    
 478
 479"""
 480[Summary]: Get shortName and displayName of each module.
 481[Returns]: Returns a set of modules.
 482"""
 483def get_modules_short_displaynames():
 484    # Let's get the resource from the DB
 485    try:
 486        conn    = mysql.connect()
 487        cursor  = conn.cursor()
 488        cursor.execute("SELECT shortname, displayName FROM module")
 489        res = cursor.fetchall()
 490    except Exception as e:
 491        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 492    
 493    short_names   = []
 494    display_names = []
 495    # Module IDs first
 496    for row in res:
 497        short_names.append(row[0])
 498        display_names.append(row[1])
 499    cursor.close()
 500    conn.close()
 501    
 502    return short_names, display_names   
 503
 504
 505"""
 506[Summary]: Finds a module.
 507[Returns]: Returns a module.
 508"""
 509@app.route('/api/module/<ID>', methods=['GET'])
 510def find_module(ID, internal_call=False):
 511    if (not internal_call):
 512        if request.method != 'GET': return
 513
 514    # Check if the user has permissions to access this resource
 515    if (not internal_call): 
 516        views.user.isAuthenticated(request)
 517    
 518    # Get the tree of the module and other relevant information.
 519    tree            = (get_module_tree(str(ID), True))
 520    recommendations = (views.recommendation.find_recommendations_of_module(ID, True))
 521    dependencies    = (views.dependency.find_dependency_of_module(ID, True))
 522    
 523    if (not dependencies): dependencies = []
 524    if (not recommendations): recommendations = []
 525
 526    # Let's get the modules from the database.
 527    try:
 528        conn    = mysql.connect()
 529        cursor  = conn.cursor()
 530        cursor.execute("SELECT ID, typeID, shortname, fullname, displayname, logicfilename, description, avatar, createdon, updatedon FROM module WHERE ID=%s", ID)
 531        res = cursor.fetchall()
 532    except Exception as e:
 533        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 534    
 535    # Check for empty results. 
 536    if (len(res) == 0):
 537        cursor.close()
 538        conn.close()
 539        if (not internal_call):
 540            return(modules.utils.build_response_json(request.path, 404))
 541        else:
 542            return(None)
 543    else:
 544        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
 545        for row in res:
 546            data = {}
 547            data['id']              = row[0]
 548            data['type_id']         = row[1]
 549            data['shortname']       = row[2]
 550            data['fullname']        = row[3]
 551            data['displayname']     = row[4]
 552            data['logic_filename']  = row[5]
 553            data['description']     = row[6]
 554            data['avatar']          = row[7]
 555            data['createdon']       = row[8]
 556            data['updatedon']       = row[9]
 557            data['tree']            = tree
 558            data['recommendations'] = recommendations and recommendations or []
 559            data['dependencies']    = dependencies
 560            datas.append(data)
 561        cursor.close()
 562        conn.close()
 563
 564        # 'May the Force be with you, young padawan'.
 565        if (not internal_call):
 566            return(modules.utils.build_response_json(request.path, 200, datas))
 567        else:
 568            return(datas)
 569
 570"""
 571[Summary]: Finds questions linked to a module.
 572[Returns]: Returns a module.
 573"""
 574@app.route('/api/module/<ID>/questions', methods=['GET'])
 575def find_module_questions(ID, internal_call=False):
 576    if (not internal_call):
 577        if request.method != 'GET': return
 578
 579    # Check if the user has permissions to access this resource
 580    if (not internal_call): views.user.isAuthenticated(request)
 581
 582
 583    # Let's get the questions of the module
 584    try:
 585        conn    = mysql.connect()
 586        cursor  = conn.cursor()
 587        cursor.execute("SELECT DISTINCT question_id, question, createdon, updatedon FROM view_module_question WHERE module_id=%s", ID)
 588        res = cursor.fetchall()
 589    except Exception as e:
 590        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 591    
 592    # Check for empty results. 
 593    if (len(res) == 0):
 594        cursor.close()
 595        conn.close()
 596        if (not internal_call):
 597            return(modules.utils.build_response_json(request.path, 404))
 598        else:
 599            return(None)
 600    else:
 601        datas = [] # Create a new nice empty array to be populated with data from the DB.
 602        for row in res:
 603            data = {}
 604            data['id']              = row[0]
 605            data['content']         = row[1]
 606            data['createdon']       = row[2]
 607            data['updatedon']       = row[3]
 608            datas.append(data)
 609        cursor.close()
 610        conn.close()
 611
 612        # 'May the Force be with you, young padawan'.
 613        if (not internal_call):
 614            return(modules.utils.build_response_json(request.path, 200, datas))
 615        else:
 616            return(datas)
 617
 618"""
 619[Summary]: Finds answers linked to a module.
 620[Returns]: Returns a module.
 621"""
 622@app.route('/api/module/<ID>/answers', methods=['GET'])
 623def find_module_answers(ID, internal_call=False):
 624    if (not internal_call):
 625        if request.method != 'GET': return
 626
 627    # Check if the user has permissions to access this resource
 628    if (not internal_call): views.user.isAuthenticated(request)
 629
 630    # Let's get the answers linked to the current module.
 631    try:
 632        conn    = mysql.connect()
 633        cursor  = conn.cursor()
 634        cursor.execute("SELECT DISTINCT answer_id, answer, createdon, updatedon FROM view_module_answers WHERE module_id=%s", ID)
 635        res = cursor.fetchall()
 636    except Exception as e:
 637        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 638    
 639    # Check for empty results. 
 640    if (len(res) == 0):
 641        cursor.close()
 642        conn.close()
 643        if (not internal_call):
 644            return(modules.utils.build_response_json(request.path, 404))
 645        else:
 646            return(None)
 647    else:
 648        datas = [] # Create a new nice empty array to be populated with data from the DB.
 649        for row in res:
 650            data = {}
 651            data['id']              = row[0]
 652            data['content']         = row[1]
 653            data['createdon']       = row[2]
 654            data['updatedon']       = row[3]
 655            datas.append(data)
 656        cursor.close()
 657        conn.close()
 658
 659        # 'May the Force be with you, young padawan'.
 660        if (not internal_call):
 661            return(modules.utils.build_response_json(request.path, 200, datas))
 662        else:
 663            return(datas)
 664
 665"""
 666[Summary]: Get the tree of the module. This tree contains all the questions and answers.
 667[Returns]: A set of questions, its children, and its answers.
 668"""
 669@app.route('/api/module/<pID>/tree', methods=['GET'])
 670def get_module_tree(pID, internal_call=False):
 671    # Do you want to add recommendations to the tree? For example, if an answer is X than the recommendation is Y, and so on. This feature is still experimental.
 672    add_recommendations_to_tree = False
 673    IDS = []
 674    if (not internal_call):
 675        if request.method != 'GET': return
 676
 677    # 1. Check if the user has permissions to access this resource
 678    if (not internal_call): views.user.isAuthenticated(request)
 679
 680    # 1.1. Check if the user needs information about all modules available on the database.
 681    if (pID.lower() == "all"):
 682        try:
 683            conn    = mysql.connect()
 684            cursor  = conn.cursor()
 685            cursor.execute("SELECT ID FROM Module ORDER BY ID ASC")
 686            res = cursor.fetchall()
 687            if (len(res) != 0):
 688                for row in res:
 689                    IDS.append(int(row[0]))
 690            cursor.close()
 691            conn.close()
 692        except Exception as e:
 693            raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 694    else:
 695        IDS.append(pID)
 696
 697    datas = []
 698    for ID in IDS:
 699        # print(" Processing Data of Module " + str(ID))
 700        # 2. Let's get the main questions of the module.
 701        try:
 702            conn    = mysql.connect()
 703            cursor  = conn.cursor()
 704            cursor.execute("SELECT module_id, module_displayname, question_id, question, questionorder, multipleAnswers FROM view_module_question WHERE module_id = %s", ID)
 705            res = cursor.fetchall()
 706        except Exception as e:
 707            raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 708        
 709        # 2.2. Check for empty results - 'Fasten your seatbelts. It's going to be a bumpy night'.
 710        if (len(res) == 0):
 711            cursor.close()
 712            conn.close()
 713            if (len(IDS) == 1):
 714                if (not internal_call):
 715                    return(modules.utils.build_response_json(request.path, 404))
 716                else:
 717                    return None
 718            else:
 719                continue
 720        else:
 721            # 2.2.1. The initial set of the information about the module.
 722            for row in res: 
 723                data = {"id": row[0], "name": row[1]}
 724                break
 725            # 2.2.2. Map questions of the module to a JSON Python Object (dic).
 726            questions = []
 727            for row in res:
 728                question = {"id": row[2], "type": "question", "name": row[3], "multipleAnswers" : row[5], "order": row[4], "children": []}
 729                questions.append(question)
 730            data.update({"tree":  questions})
 731
 732            cursor.close()
 733            conn.close()
 734        
 735        # 3. Recursively get each question child and corresponding data (question, answer, and so on).
 736        for question in data['tree']: 
 737            get_children(True, question, add_recommendations_to_tree)
 738             
 739
 740        if (len(IDS) == 1):    
 741            # 4. 'May the Force be with you'.
 742            if (not internal_call):
 743                return(modules.utils.build_response_json(request.path, 200, data))
 744            else:
 745                #del data[request.path]
 746                return(data['tree'])
 747        else:
 748            datas.append(data)
 749    
 750    # 4. 'May the Force be with you'.
 751    if (not internal_call):
 752        return(modules.utils.build_response_json(request.path, 200, datas))
 753    else:
 754        del datas[request.path]
 755        # print("### = " + str(datas['tree']))
 756        return(datas['tree'])
 757
 758
 759"""
 760[Summary]: Auxiliary function to check if a subquestion is no longer linked to parent one. 
 761[Arguments]:
 762    - $parent$: Parent id.
 763    - $values$: Child id.
 764    - $trigger$; Answer id. 
 765"""
 766def subquestion_parent_changed(parent, child, trigger):
 767    try:
 768        conn    = mysql.connect()
 769        cursor  = conn.cursor()
 770        print("SELECT ID FROM question_has_child WHERE parent=%s AND child=%s AND ontrigger=%s", (parent, child, trigger))
 771        cursor.execute("SELECT ID FROM question_has_child WHERE parent=%s AND child=%s AND ontrigger=%s", (parent, child, trigger))
 772        res = cursor.fetchall()
 773    except Exception as e:
 774        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 775
 776    # 2.1. Check for empty results. 
 777    if (len(res) == 0):
 778        cursor.close()
 779        conn.close()
 780        return(True)    
 781    else:
 782        cursor.close()
 783        conn.close()
 784        return(False)
 785
 786"""
 787[Summary]: Auxiliary functions to check if an answer or question is no longer is parent or child.
 788[Arguments]:
 789    - $question_id$: Parent id.
 790    - $answer_id$: Child id.
 791"""
 792def parent_changed(question_id, answer_id):
 793    try:
 794        conn    = mysql.connect()
 795        cursor  = conn.cursor()
 796        print("SELECT ID FROM question_answer WHERE questionID=%s AND answerID=%s", (question_id, answer_id))
 797        cursor.execute("SELECT ID FROM question_answer WHERE questionID=%s AND answerID=%s", (question_id, answer_id))
 798        res = cursor.fetchall()
 799    except Exception as e:
 800        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 801
 802    # 2.1. Check for empty results. 
 803    if (len(res) == 0):
 804        cursor.close()
 805        conn.close()
 806        return(True)    
 807    else:
 808        cursor.close()
 809        conn.close()
 810        return(False)
 811
 812"""
 813[Summary]: When a new question or answer is added on the client side an ID is generated for each one. After that, the user is required to add recommendations. 
 814           These recommendations will be given taking into account if the user has selected a set of answers to a set of questions. The mapping of questions and 
 815           answers temporarily uses the previous generated ID. After the process of adding each question and answer to the database, that temporary ID needs to be 
 816           updated to the new one (i.e., the ID that identifies each question and answer on the database). 
 817[Arguments]:
 818    - $client_id$: The id temporary assigned (on the client side) to a question or answer.
 819    - $database_id$: The 'real' id of the question or answer that is used to update a recommendation.
 820    - $recommendations$: The list of recommendations.
 821    - $is_question$: Flag to ascertain if the mapping operation of client_id => database_id is to be performed on a question or an answer.
 822"""
 823def update_questions_answers_ids(client_id, database_id, recommendations, is_question):
 824    DEBUG = False
 825    if (DEBUG):
 826        if (is_question):
 827            print("[SAM-API] update_questions_answers_ids() => Trying to find client_question_id = " + str(client_id) + " in recommendations list.")
 828        else:
 829            print("[SAM-API] update_questions_answers_ids() => Trying to find client_question_id = " + str(client_id) + " in recommendations list.")
 830    
 831    if (recommendations):
 832        for recommendation in recommendations:
 833            # if ("questions_answers" not in recommendation): continue
 834            for question_answer in recommendation['questions_answers']:
 835                # Update the questions to the real ID
 836                if (is_question):
 837                    if ("client_question_id" in question_answer):
 838                        if (question_answer['client_question_id'] == client_id):
 839                            del question_answer['client_question_id']
 840                            question_answer['question_id'] = database_id
 841                            if (DEBUG): print("[SAM-API] update_questions_answers_ids() => Found it, updating node = " + str(question_answer))
 842                else:
 843                    if ("client_answer_id" in question_answer):
 844                        if (question_answer['client_answer_id'] == client_id):
 845                            del question_answer['client_answer_id']
 846                            question_answer['answer_id'] = database_id
 847                            if (DEBUG): print("[SAM-API] update_questions_answers_ids() => Found it, updating node = " + str(question_answer))
 848
 849"""
 850[Summary]: Iterates the module tree that contains the mapping of questions and answers. This is an auxiliary function of add_module() and update_module().
 851[Arguments]:
 852    - $module_id$: Id of the newly created module.
 853    - $node$:  current node of the tree being processed. 
 854    - $p_node$: Previous node or parent node.
 855    - $p_p_node$: Parent of the parent node.
 856"""
 857def iterate_tree_nodes(recommendations, operation, module_id, c_node, p_node=None, p_p_node=None):
 858    debug=False
 859    # print("[SAM-API] Processing current node = '"+ str(c_node['name'])+"'")
 860    operation = operation.upper()
 861
 862    # In the case of an UPDATE operation, we need to check if this question is available on the database; if not, 
 863    # this means that the user has added a new question/answer and the values are in need of being processed with an INSERT operation.
 864    # ---> We need to change the operation from UPDATE TO INSERT after encountering this situation. 
 865    # ---> We can check if a question/answer is NOT available on the database if c_node['id'] == null
 866    if (operation == "UPDATE" and (not c_node['id'])): 
 867        operation = "INSERT"
 868
 869    # 1. Check if the current node is a question or an answer.
 870    if (c_node["type"] == "question"): 
 871        
 872        # 1.1. Add or update question - Table [Question].
 873        if (operation == "INSERT"):
 874            sql     = "INSERT INTO question (content, multipleAnswers) VALUES (%s, %s)"
 875            values  =  (c_node['name'], c_node['multipleAnswers'])
 876            exists_flag = False
 877            # Check if the question already exists (i.e. if c_node['id'] == null)
 878            if (not c_node['id']):
 879                c_node.update({"id": modules.utils.db_execute_update_insert(mysql, sql, values)})
 880                # By knowing the client_id update recommendations questions and answers to the database id (c_node['id']).
 881                update_questions_answers_ids(c_node['client_id'], c_node['id'], recommendations, True)
 882
 883            if (debug): print("  -> [" + str(c_node["id"]) + "] = '" + sql + ", " + str(values))
 884        if (operation == "UPDATE"):
 885            sql     = "UPDATE question SET content=%s, multipleAnswers=%s WHERE ID=%s"
 886            values  = (c_node['name'], c_node['multipleAnswers'], c_node['id'])
 887            if (debug): print("  -> [" + str(c_node["id"]) + "] = '" + sql + ", " + str(values))
 888            modules.utils.db_execute_update_insert(mysql, sql, values)
 889
 890        
 891        # 1.2. Add link to table [Module_Question] - Link question and the module together.
 892        # Be aware, that child questions are not added to this table. That is, only questions are mapped to modules.
 893        if (p_node is None):
 894            if (operation == "INSERT"):
 895                sql     = "INSERT INTO module_question (moduleID, questionID, questionOrder) VALUES (%s, %s, %s)"
 896                values  = (module_id,c_node['id'], 0)
 897                modules.utils.db_execute_update_insert(mysql, sql, values)
 898                if (debug): print("  -> [?] = '" + sql + ", " + str(values))
 899
 900        # 1.3. Add Sub question to table [Question_has_Child] - Link question and subquestions by a trigger (i.e., answer).
 901        # Knowing that the current node is a parent, this is accomplish by checking if the parent was an answer. 
 902        if (p_node != None):
 903            if (p_node['type'] == "answer"): 
 904
 905                if (operation == "INSERT"):
 906                    sql     = "INSERT INTO question_has_child (parent, child, ontrigger, questionOrder) VALUES (%s, %s, %s, %s)"
 907                    values  = (p_p_node['id'], c_node['id'], p_node['id'], 0)
 908                    modules.utils.db_execute_update_insert(mysql, sql, values)
 909                    if (debug): print("  -> [?] = '" + sql + ", " + str(values))
 910                
 911                if (operation == "UPDATE"):
 912                    if (subquestion_parent_changed(p_p_node['id'], c_node['id'], p_node['id'])):
 913                        if (debug): print("  -> [?] New Link detected")
 914                        # Remove the previous link
 915                        sql     = "DELETE FROM question_has_child WHERE child=%s"
 916                        values  = c_node['id']
 917                        if (debug): print("     -> '" + sql + ", " + str(values))
 918                        modules.utils.db_execute_update_insert(mysql, sql, values)
 919                        # Add the new link
 920                        sql     = "INSERT INTO question_has_child (parent, child, ontrigger, questionOrder) VALUES (%s, %s, %s, %s)"
 921                        values  = (p_p_node['id'], c_node['id'], p_node['id'], 0)
 922                        if (debug): print("     -> '" + sql + ", " + str(values))
 923                        modules.utils.db_execute_update_insert(mysql, sql, values)
 924    else: 
 925        # 1.1. Add or update answer - Table [Answer].
 926        if (operation == "INSERT"):
 927            sql     = "INSERT INTO answer (content) VALUES (%s)"
 928            values  = c_node['name']
 929            exists_flag = False
 930            # Check if the answer already exists (i.e. if c_node['id'] == null)
 931            if (not c_node['id']):
 932                # Check if the answer is similar or equal to one already available on the database, if so, use the id of the one that is equal
 933                # This is performed by checking the contents of an answer. No need to create a new answer on the database if one similar is already available.
 934                node_id = (modules.utils.db_already_exists(mysql, "SELECT id, content FROM answer WHERE content LIKE %s", c_node['name']))
 935                print(node_id)
 936                if (node_id == -1):
 937                    c_node.update({"id": modules.utils.db_execute_update_insert(mysql, sql, values)}) # Store the ID of the newly created answer.
 938                else:
 939                    c_node.update({"id": node_id}) # Store the ID of an answer that was previsouly inserted on the database. 
 940
 941                # By knowing the client_id update recommendations questions and answers to the database id (c_node['id']).
 942                update_questions_answers_ids(c_node['client_id'], c_node['id'], recommendations, False)
 943
 944            if (debug): print("  -> [" + str(c_node["id"]) + "] = '" + sql + ", " + str(values))
 945        
 946        if (operation == "UPDATE"):
 947            sql     = "UPDATE answer SET content=%s WHERE ID=%s"
 948            values  = (c_node['name'], c_node['id'])
 949            if (debug): print("  -> [" + str(c_node["id"]) + "] = '" + sql + ", " + str(values))
 950            modules.utils.db_execute_update_insert(mysql, sql, values)
 951        
 952        if (operation == "INSERT"):
 953            # 1.2. Add link to table [Question_Answer] - Link question and answer together.
 954            sql     = "INSERT INTO question_answer (questionID, answerID) VALUES (%s, %s)"
 955            values  = (p_node['id'], c_node['id'])
 956            modules.utils.db_execute_update_insert(mysql, sql, values)
 957            if (debug): print("  -> [?] = '" + sql + ", " + str(values))
 958        
 959        if (operation == "UPDATE"):
 960            # Check if the link between the current answer node and a question was changed (i.e., parent change).
 961            # If true remove the previous link and assigned the new one
 962            if (parent_changed(p_node['id'], c_node['id'])):
 963                # Remove the previous one
 964                if (debug): print("  -> [?] New Link detected")
 965                sql     = "DELETE FROM question_answer WHERE answerID=%s AND questionID IN (SELECT questionID From question_answer WHRE answerID=%s)"
 966                values  = (c_node['id'], c_node['id'])
 967                if (debug): print("    -> [?] = '" + sql + ", " + str(values))
 968                modules.utils.db_execute_update_insert(mysql, sql, values)
 969                # Add new updated link
 970                sql     = "INSERT INTO question_answer (questionID, answerID) VALUES (%s, %s)"
 971                values  = (p_node['id'], c_node['id'])
 972                modules.utils.db_execute_update_insert(mysql, sql, values)
 973                if (debug): print("    -> [?] = '" + sql + ", " + str(values))
 974
 975    if (debug): print("\n")
 976    try:
 977        # Recursively iterate 
 978        for child in c_node['children']:
 979            iterate_tree_nodes(recommendations, operation, module_id, child, c_node, p_node)
 980    except:
 981        pass
 982    return
 983
 984"""
 985[Summary]: Auxiliary function to get the sub-questions of a set of questions, and the sub-questions of 
 986           those, and so on. This is accomplished through our 'friendly neighbor' - recursivity. 
 987[Arguments]:
 988    - $initial$:  This flag must be set to true if it is the initial call of the recursive function.
 989    - $c_childs$: Array that contains the childs of a question ($question_js$ Python object), this must be initially empty.
 990    - $question$: Current question being analysed (has childs ?).
 991    - $question_js$: The JSON Python object being populate with data (question information, answers, and so on.)
 992[Returns]: Returns a JSON object with the mapping of all childs of a set of questions.
 993"""
 994def get_children(initial, question, add_recommendations_to_tree):
 995    children = question['children']
 996    debug = False
 997    if (debug): print("# Parsing question [%s] - %s" % (question['id'], question['name']))
 998    # 1. Get answers of the parent question.
 999    try:
1000        conn    = mysql.connect()
1001        cursor  = conn.cursor()
1002        cursor.execute("SELECT answer_id, answer FROM view_question_answer WHERE question_id=%s", question['id'])
1003        res = cursor.fetchall()
1004    except Exception as e:
1005        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
1006    
1007    # 1.2. Store, if available, answers of the parent question - An answer is a child of a parent question.
1008    if (len(res) != 0):
1009        for row in res:
1010            answer = { "id": row[0], "type": "answer", "name": row[1], "children" : []}
1011            children.append(answer)     
1012    cursor.close() # Clean the house nice & tidy.
1013
1014    # 2. Check if the current question has children; IF not, return.
1015    #    This is the illusive break/return condition of this recursive function - 'Elementary, my dear Watson.'
1016    if (len(children) == 0): return None
1017
1018    # Get recommendation(s) for the current answer and question.
1019    for child in children:
1020        if (child['type'] == 'question'): continue
1021
1022
1023    # 3. Get sub-questions triggered by an answer 
1024    for child in children:
1025        try:
1026            cursor  = conn.cursor()
1027            cursor.execute("SELECT child_id, question, questionOrder, ontrigger, multipleAnswers FROM view_question_childs WHERE parent_id=%s AND ontrigger=%s", (question['id'], child['id']))
1028            res = cursor.fetchall()
1029        except Exception as e:
1030            raise modules.error_handlers.BadRequest(request.path, str(e), 500)
1031        
1032        # 3.1. Store it!
1033        if (len(res) != 0):
1034            for row in res:
1035                s_question = {"id": row[0], "name": row[1], "multipleAnswers": row[4], "type": "question","order": row[2],"trigger": row[3], "children": []}
1036                child['children'].append(s_question)
1037        
1038        # 3.2. If requested, and a sub-question is no where to be found for the current child, the list of recommendations will be added as children to the current answer.
1039        else:
1040            if (add_recommendations_to_tree):
1041                recommendations = (views.recommendation.find_recommendations_of_question_answer(question['id'], child['id'], True)).json
1042                # print(str(recommendations))
1043                if (recommendations != None): child['children'] = recommendations
1044            
1045            
1046    cursor.close()
1047    conn.close()
1048
1049    # Debug
1050    if (debug):
1051        print("<!> Question [%s] ('%s') has the following answers:" % (question['id'], question['name']))
1052        for answer in children:
1053            if (answer['type'] == "answer"):
1054                print("  -> Answer ID[%d] - %s" % (answer['id'], answer['name']))
1055                for s_question in answer['children']:
1056                    print("     -> Question ID[%d] - %s" % (s_question['id'], s_question['name']))
1057
1058    # 4. Recursive calls and python JSON object construction
1059    if (len(question['children']) != 0): question.update({"expanded": False})
1060    for answer in children:
1061        t_answer = answer
1062        # We need to go deeper in order to find the questions to be parsed to this recursive function.
1063        for question in answer['children']:
1064            t_answer.update({"expanded": False})
1065            if (question['type'] != 'recommendation'):
1066                get_children(False, question, add_recommendations_to_tree)
1067 
1068"""
1069[Summary]: Checks if a module is a plugin.
1070[Returns]: Returns a Boolean.
1071"""
1072@app.route('/api/module/<ID>/type', methods=['GET'])
1073def check_plugin (ID, internal_call=False):
1074    if (not internal_call):
1075        if request.method != 'GET': return
1076
1077    # Check if the user has permissions to access this resource
1078    if (not internal_call): views.user.isAuthenticated(request)
1079
1080    tree = get_module_tree(str(ID), True)
1081
1082    if (tree == None):
1083        plugin = True
1084    else:
1085        plugin = False
1086
1087    if (not internal_call):
1088        return modules.utils.build_response_json(request.path, plugin)
1089    else:
1090        return plugin

Get Modules

GET /api/module
Synopsis

Get the list of modules installed in the platform.

Request Headers
Response Headers
Response JSON Object
  • id (int) – Id of the module.

  • type_id (int) – Module type id.

  • shortname (string) – Unique short name or abbreviation.

  • description (string) – Module description.

  • fullname (string) – Full name of the module.

  • displayname (string) – Module display name that can be used by a frontend.

  • avatar (string) – Avatar of the user (i.e., location in disk).

  • logic_filename (string) – Filename of the file containing the dynamic logic of the module.

  • plugin (boolean) – A flag that sets if the current module is a plugin.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/modules":{
      "content":[
         {
            "id":1,
            "type_id":null,
            "shortname":"SRE",
            "fullname":"Security Requirements Elicitation",
            "description":null,
            "displayname":"Security Requirements",
            "avatar":null,
            "logic_filename":null,
            "plugin":false,
            "createdon":"Tue, 28 Sep 2021 13:42:48 GMT",
            "updatedon":"Tue, 28 Sep 2021 13:48:19 GMT"
         }
      ],
      "status":200
   }
}

GET /api/module/(int: id)
Synopsis

Get a module identified by id.

Request Headers
Response Headers
Parameters
  • id – Id of the module.

Response JSON Object
  • id (int) – Id of the module.

  • type_id (int) – Module type id.

  • dependencies (array) – An array that contains the set of modules that the current module depends on.

  • shortname (string) – Unique short name or abbreviation.

  • displayname (string) – Module display name that can be used by a frontend.

  • fullname (string) – Full name of the module.

  • description (string) – Module description.

  • logic_filename (string) – Filename of the file containing the dynamic logic of the module.

  • avatar (string) – Avatar of the user (i.e., location in disk).

  • recommendations (array) – Set of recommendations mapped to this module.

  • tree (array) – An array that contains the set of questions and answers mapped for the current module.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/module/1":{
      "content":[
         {
            "id":1,
            "type_id":null,
            "dependencies":[],
            "shortname":"SRE",
            "displayname":"Security Requirements",
            "fullname":"Security Requirements Elicitation",
            "description":null,
            "logic_filename":null,
            "avatar":null,
            "recommendations":[
               {
                  "id":1,
                  "content":"Confidentiality",
                  "questions_answers":[
                     {
                        "id":1,
                        "answer_id":1,
                        "question_id":1,
                        "createdon":"Fri, 19 Nov 2021 12:54:49 GMT",
                        "updatedon":"Fri, 19 Nov 2021 12:54:49 GMT"
                     }
                  ],
                  "createdon":"Fri, 19 Nov 2021 12:54:49 GMT",
                  "updatedon":"Fri, 19 Nov 2021 12:54:49 GMT"
               }
            ],
            "tree":[
               {
                  "id":1,
                  "order":0,
                  "name":"What is the domain of your IoT system ?",
                  "multipleAnswers":0,
                  "type":"question",
                  "children":[
                     {
                        "id":1,
                        "name":"Smart home",
                        "type":"answer",
                        "children":[],
                     },
                     {
                        "id":2,
                        "name":"Smart Healthcare",
                        "type":"answer",
                        "children":[]
                     }
                  ],
               }
            ],
            "createdon":"Fri, 19 Nov 2021 12:54:49 GMT",
            "updatedon":"Fri, 19 Nov 2021 12:54:49 GMT"
         }
      ],
      "status":200
   }
}

Get Module Type

GET /api/module/(int: id)/type
Synopsis

Get the type of module identified by id.

Request Headers
Response Headers
Parameters
  • id – Id of the module.

Response JSON Object
  • status (boolean) – A flag that sets if the current module is a plugin, true if the correct module is a plugin, false otherwise.

Status Codes

Note

A module plugin comprises questions and answers, while a module that is not a plugin fully depends on other modules to produce output.

Example Response

{
   "/api/module/1/type":{
      "status":false
   }
}

Get Questions/Answers of a Module

GET /api/module/(int: id)/tree
Synopsis

Get the tree array of questions and answers of a module identified by id.

Request Headers
Response Headers
Parameters
  • id – Id of the module.

Response JSON Object
  • id (int) – Id of the current question or answer in the array depending of the type.

  • order (int) – The sequencial number of the question.

  • children (array) – The current module has a set of questions or answers that are mapped to this array.

  • name (string) – The content/text of the current question.

  • type (string) – Defines if the current element in the tree array is a question or an answer.

  • multipleAnswers (boolean) – Defines if the user can select multiple answers for the current question.

  • status (int) – status code.

Status Codes

Note

Please, consider using something like, for example, the react-sortable-tree react component to develop a proper user interface to interact and manage the data returned by this service.

Example Response

{
   "/api/module/1/tree":{
      "tree":[
         {
            "id":1,
            "order":0,
            "name":"What is the domain of your IoT system ?",
            "multipleAnswers":0,
            "type":"question"
            "children":[
               {
                  "id":1,
                  "name":"Smart home",
                  "type":"answer",
                  "children":[]
               },
               {
                  "id":2,
                  "name":"Smart Healthcare",
                  "type":"answer",
                  "children":[]
               }
            ],
         }
      ],
      "status":200
   }
}

Get Questions of Modules

GET /api/module/questions
Synopsis

Get the list of questions mapped to each module.

Request Headers
Response Headers
Response JSON Object
  • id (int) – Module or question id.

  • questions (array) – Array of questions.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/modules/questions": {
      "content": [
         {
         "id": 1,
         "questions": [
            {
               "id": 1,
               "content": "What is the domain of your IoT system ?",
               "createdon": "Fri, 19 Nov 2021 15:29:18 GMT",
               "updatedon": "Fri, 19 Nov 2021 15:29:18 GMT"
            }
         ]
         }
      ],
      "status": 200
   }
}

GET /api/module/(int: id)/questions
Synopsis

Get the list of questions mapped to a module identified by id.

Request Headers
Response Headers
Parameters
  • id – Id of the module.

Response JSON Object
  • id (int) – Module or question id.

  • questions (array) – Array of questions.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/modules/questions": {
      "content": [
         {
         "id": 1,
         "questions": [
            {
               "id": 1,
               "content": "What is the domain of your IoT system ?",
               "createdon": "Fri, 19 Nov 2021 15:29:18 GMT",
               "updatedon": "Fri, 19 Nov 2021 15:29:18 GMT"
            }
         ]
         }
      ],
      "status": 200
   }
}

Get Modules Answers

GET /api/module/answers
Synopsis

Get the list of answers mapped to each module.

Request Headers
Response Headers
Response JSON Object
  • id (int) – Module or answer id.

  • answers (array) – Array of answers.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/modules/answers":{
      "content":[
         {
            "id":1
            "answers":[
               {
                  "id":1,
                  "content":"Smart home",
                  "createdon":"Fri, 19 Nov 2021 15:29:18 GMT",
                  "updatedon":"Fri, 19 Nov 2021 15:29:18 GMT"
               },
               {
                  "id":2,
                  "content":"Smart Healthcare",
                  "createdon":"Fri, 19 Nov 2021 15:29:18 GMT",
                  "updatedon":"Fri, 19 Nov 2021 15:29:18 GMT"
               }
            ]
         }
      ],
      "status":200
   }
}

GET /api/module/(int: id)/answers
Synopsis

Get the list of answers mapped to a module identified by id.

Request Headers
Response Headers
Parameters
  • id – Id of the module.

Response JSON Object
  • id (int) – Module or answer id.

  • answers (array) – Array of answers.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/modules/answers":{
      "content":[
         {
            "id":1
            "answers":[
               {
                  "id":1,
                  "content":"Smart home",
                  "createdon":"Fri, 19 Nov 2021 15:29:18 GMT",
                  "updatedon":"Fri, 19 Nov 2021 15:29:18 GMT"
               },
               {
                  "id":2,
                  "content":"Smart Healthcare",
                  "createdon":"Fri, 19 Nov 2021 15:29:18 GMT",
                  "updatedon":"Fri, 19 Nov 2021 15:29:18 GMT"
               }
            ]
         }
      ],
      "status":200
   }
}

Add Module

POST /api/module
Synopsis

Add a new module.

Request Headers
Response Headers
Request JSON Object
  • shortname (string) – Unique short name or abbreviation of the module.

  • fullname (string) – Full name of the module.

  • displayname (string) – Module display name that can be used by a frontend.

  • description (string) – Module description.

  • tree (array) – Array of questions and answers mapped to the module.

  • recommendations (array) – Array of recommendations mapped to a question id and answer id.

  • dependencies (array) – An array that contains the set of modules that the current module depends on.

Status Codes

Important

The client_id field is a temporary id for a question or an answer that may not yet exist in the database – This temporary id is assigned by a client. An identical situation exists for client_question_id and client_answer_id:

Note

Each question can trigger a set of answers or questions (tree field).

Note

Each module can only be triggered if a module dependency was satisfied (i.e., the module described in the field dependencies was previously executed by the user).

Example Request

{
   "shortname":"SRE",
   "fullname":"Security Requirements Elicitation",
   "displayname":"Security Requirements",
   "description":"Module description",
   "tree":[
      {
         "id":null,
         "client_id":0,
         "type":"question",
         "name":"What is the domain of your IoT system ?",
         "multipleAnswers":false,
         "children":[
            {
               "id":null,
               "client_id":1,
               "name":"Smart home",
               "type":"answer",
               "multipleAnswers":false,
               "children":[
                  {
                     "id":null,
                     "client_id":2,
                     "name":"Will the sytem have a user LogIn ?",
                     "type":"question",
                     "children":[
                        {
                           "id":null,
                           "client_id":3,
                           "name":"Yes",
                           "type":"answer",
                           "children":[

                           ]
                        },
                        {
                           "id":null,
                           "client_id":4,
                           "name":"No",
                           "type":"answer",
                           "children":[

                           ]
                        }
                     ]
                  }
               ]
            },
            {
               "id":null,
               "client_id":5,
               "name":"Smart Healthcare",
               "type":"answer",
               "multipleAnswers":false,
               "children":[
                  {
                     "id":null,
                     "client_id":6,
                     "name":"Yes",
                     "type":"answer",
                     "children":[

                     ]
                  },
                  {
                     "id":null,
                     "client_id":7,
                     "name":"No",
                     "type":"answer",
                     "children":[

                     ]
                  }
               ]
            }
         ]
      }
   ],
   "recommendations":[
      {
         "id":null,
         "content":"Confidentiality",
         "questions_answers":[
            {
               "client_question_id":0,
               "client_answer_id":1
            }
         ]
      }
   ],
   "dependencies":[
      {
         "module":{
            "id":2
         }
      }
   ]
}

Edit Module

PUT /api/module
Synopsis

Edit a module.

Request Headers
Response Headers
Request JSON Object
  • id (int) – Id of the module to edit.

  • shortname (string) – Unique short name or abbreviation of the module.

  • fullname (string) – Full name of the module.

  • displayname (string) – Module display name that can be used by a frontend.

Response JSON Object
  • status (int) – Status code

Status Codes

Example Request

{
   "id":1,
   "shortname":"SREU",
   "fullname":"Security Requirements Elicitation Updated",
   "displayname":"Security Requirements Updated"
}

Note

Questions, answers, recommendations and dependencies of the module can also be updated using this service by following the same JSON object provide as example in the /api/module (add module) service.

Example Response

{"/api/module":{"status":200}}

Remove Module

DELETE /api/module/(int: id)
Synopsis

Performs a partial delete of a Module. That is, only the module and those sessions linked are deleted - Linked questions and associated answers are not delete.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the module to partially delete.

Status Codes
DELETE /api/module/(int: id)/full
Synopsis

Fully removes a module (including sessions, linked questions, and answers).

Request Headers
Response Headers
Parameters
  • id (int) – Id of the module to fully delete.

Status Codes
DELETE /api/module/(int: id)/questions
Synopsis

Removes all questions mapped to the module identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the module.

Status Codes
DELETE /api/module/(int: id)/answers
Synopsis

Removes all answers mapped to the module identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the module.

Status Codes
DELETE /api/module/(int: id)/logic
Synopsis

Removes the logic file mapped to the module identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the module.

Status Codes

Dependencies Services API

This section includes details concerning services developed for the dependency entity implemented in
dependency.py.
  1"""
  2// ---------------------------------------------------------------------------
  3//
  4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
  5//
  6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
  7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
  8//
  9//  This program is free software: you can redistribute it and/or modify
 10//  it under the terms of the GNU General Public License as published by
 11//  the Free Software Foundation, either version 3 of the License, or
 12//  (at your option) any later version.
 13//
 14//  This program is distributed in the hope that it will be useful,
 15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
 16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17//  GNU General Public License for more details.
 18//
 19//  You should have received a copy of the GNU General Public License
 20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 21// 
 22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
 23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
 24//  POCI-01-0145-FEDER-030657) 
 25// ---------------------------------------------------------------------------
 26"""
 27from api import app, mysql
 28from flask import request
 29import modules.error_handlers, modules.utils # SAM's modules
 30import views.user, views.module # SAM's views
 31
 32"""
 33[Summary]: Adds a new dependency to the database.
 34[Returns]: Response result.
 35"""
 36@app.route('/api/dependency', methods=['POST'])
 37def add_dependency(json_internal_data=None, internal_call=False):
 38    DEBUG=True
 39    if (not internal_call):
 40        if request.method != 'POST': return
 41    
 42    # Check if the user has permissions to access this resource
 43    if (not internal_call): views.user.isAuthenticated(request)
 44
 45    if (not internal_call):
 46        json_data = request.get_json()
 47    else:
 48        json_data = json_internal_data
 49
 50    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
 51    if (json_data is None): 
 52        if (not internal_call):
 53            return(modules.utils.build_response_json(request.path, 400))
 54        else:
 55            modules.utils.console_log("[POST]/api/dependency", "json_data is None")
 56            return(None)
 57   
 58    # Validate if the necessary data is on the provided JSON 
 59    if (not modules.utils.valid_json(json_data, {"module_id", "depends_on"})):
 60        if (not internal_call):
 61            raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
 62        else:
 63            modules.utils.console_log("[POST]/api/dependency", "Some required key or value is missing from the JSON object")
 64
 65    module_id   = json_data['module_id']
 66    depends_on  = json_data['depends_on']
 67    createdon   = "createdon" in json_data and json_data['createdon'] or None
 68    updatedon   = "updatedon" in json_data and json_data['updatedon'] or None
 69    
 70    # Build the SQL instruction using our handy function to build sql instructions.
 71    values  = (module_id, depends_on, createdon, updatedon)
 72    columns = ["moduleID", "dependsOn", createdon and "createdon" or None, updatedon and "updatedon" or None]
 73    sql, values = modules.utils.build_sql_instruction("INSERT INTO dependency", columns, values)
 74    
 75    # Add
 76    n_id = modules.utils.db_execute_update_insert(mysql, sql, values, True)
 77
 78    if (n_id is None):
 79        if (not internal_call):
 80            return(modules.utils.build_response_json(request.path, 400))
 81        else:
 82            return(None)
 83    else:
 84        if (not internal_call):
 85            return(modules.utils.build_response_json(request.path, 200, {"id": n_id}))
 86        else:
 87            return(n_id)
 88
 89"""
 90[Summary]: Updates a dependency.
 91[Returns]: Response result.
 92"""
 93@app.route('/api/dependency', methods=['PUT'])
 94def update_dependency(json_internal_data=None, internal_call=False):
 95    DEBUG=True
 96    if (not internal_call):
 97        if request.method != 'PUT': return
 98    
 99    # Check if the user has permissions to access this resource
100    if (not internal_call): views.user.isAuthenticated(request)
101
102    if (not internal_call):
103        json_data = request.get_json()
104    else:
105        json_data = json_internal_data
106    
107    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
108    if (json_data is None): 
109        if (not internal_call):
110            return(modules.utils.build_response_json(request.path, 400)) 
111        else:
112            modules.utils.console_log("[PUT]/api/dependency", "json_data is None")
113            return(None)
114
115    dependency_id_available = True
116    # Validate if the necessary data is on the provided JSON 
117    if (not modules.utils.valid_json(json_data, {"id"})):
118        dependency_id_available = False
119        if (not modules.utils.valid_json(json_data, {"module_id", "depends_on", "p_depends_on"})):
120            raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
121        
122
123    module_id     = "module_id"     in json_data and json_data['module_id'] or None
124    depends_on    = "depends_on"    in json_data and json_data['depends_on'] or None     # New dependency
125    p_depends_on  = "p_depends_on"  in json_data and json_data['p_depends_on'] or None   # Previous dependency
126    createdon     = "createdon"     in json_data and json_data['createdon'] or None
127    updatedon     = "updatedon"     in json_data and json_data['updatedon'] or None
128
129    # IF required, get the ID of the dependency taking into account the id of the module and the id of the module that it depends on.
130    if (not dependency_id_available):
131        json_tmp = find_dependency_of_module_2(module_id, p_depends_on, True)
132        if (not json_tmp):
133            raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the temporary JSON object", 400)    
134        json_data['id'] = json_tmp['id']
135    
136    # Build the SQL instruction using our handy function to build sql instructions.
137    values  = (module_id, depends_on, createdon, updatedon)
138    columns = [module_id and "moduleID" or None, depends_on and "dependsOn" or None, createdon and "createdon" or None, updatedon and "updatedOn" or None]
139    where   = "WHERE id="+str(json_data['id'])
140
141    # Check if there is anything to update (i.e. frontend developer has not sent any values to update).
142    if (len(values) == 0): return(modules.utils.build_response_json(request.path, 200))   
143
144    sql, values = modules.utils.build_sql_instruction("UPDATE dependency", columns, values, where)
145    if (DEBUG): modules.utils.console_log("[PUT]/api/dependency", sql + " " + str(values))
146
147    # Update resource
148    modules.utils.db_execute_update_insert(mysql, sql, values)
149
150    if (not internal_call):
151        return(modules.utils.build_response_json(request.path, 200))
152    else:
153        return(True)
154
155"""
156[Summary]: Get dependencies.
157[Returns]: Response result.
158"""
159@app.route('/api/dependencies')
160def get_dependency():
161    if request.method != 'GET': return
162
163    # 1. Check if the user has permissions to access this resource
164    views.user.isAuthenticated(request)
165
166    # 2. Let's get the answeers for the question from the database.
167    try:
168        conn    = mysql.connect()
169        cursor  = conn.cursor()
170        cursor.execute("SELECT ID, moduleID, dependsOn, createdOn, UpdatedOn FROM dependency")
171        res = cursor.fetchall()
172    except Exception as e:
173        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
174    
175    # 2.2. Check for empty results 
176    if (len(res) == 0):
177        cursor.close()
178        conn.close()
179        return(modules.utils.build_response_json(request.path, 404))    
180    else:
181        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
182        for row in res:
183            data = {}
184            data['id']          = row[0]
185            data['module_id']   = row[1]
186            data['depends_on']  = row[2]
187            data['createdon']   = row[3]
188            data['updatedon']   = row[4]
189            datas.append(data)
190        cursor.close()
191        conn.close()
192        # 3. 'May the Force be with you, young master'.
193        return(modules.utils.build_response_json(request.path, 200, datas)) 
194
195"""
196[Summary]: Finds a dependency.
197[Returns]: Response result.
198"""
199@app.route('/api/dependency/<ID>', methods=['GET'])
200def find_dependency(ID):
201    if request.method != 'GET': return
202
203    # 1. Check if the user has permissions to access this resource
204    views.user.isAuthenticated(request)
205
206    # 2. Let's get the answeers for the question from the database.
207    try:
208        conn    = mysql.connect()
209        cursor  = conn.cursor()
210        cursor.execute("SELECT ID, moduleID, dependsOn, createdOn, UpdatedOn FROM dependency WHERE ID=%s", ID)
211        res = cursor.fetchall()
212    except Exception as e:
213        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
214    
215    # 2.2. Check for empty results 
216    if (len(res) == 0):
217        cursor.close()
218        conn.close()
219        return(modules.utils.build_response_json(request.path, 404))    
220    else:
221        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
222        for row in res:
223            data = {}
224            data['id']          = row[0]
225            data['module_id']   = row[1]
226            data['depends_on']  = row[2]
227            data['createdon']   = row[3]
228            data['updatedon']   = row[4]
229            datas.append(data)
230        cursor.close()
231        conn.close()
232        # 3. 'May the Force be with you, young master'.
233        return(modules.utils.build_response_json(request.path, 200, datas)) 
234
235"""
236[Summary]: Finds a dependency of a module
237[Returns]: Response result.
238"""
239@app.route('/api/dependency/module/<ID>', methods=['GET'])
240def find_dependency_of_module(ID, internal_call=False):
241    if (not internal_call):
242        if request.method != 'GET': return
243
244    # 1. Check if the user has permissions to access this resource
245    if (not internal_call): views.user.isAuthenticated(request)
246
247    # 2. Let's get the answeers for the question from the database.
248    try:
249        conn    = mysql.connect()
250        cursor  = conn.cursor()
251        cursor.execute("SELECT dependency_id, depends_module_id, createdOn, updatedOn FROM view_module_dependency WHERE module_ID=%s", ID)
252        res = cursor.fetchall()
253    except Exception as e:
254        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
255    
256    # 2.2. Check for empty results 
257    if (len(res) == 0):
258        cursor.close()
259        conn.close()
260        if (not internal_call):
261            return(modules.utils.build_response_json(request.path, 404)) 
262        else:
263            return None
264    else:
265        datas = []
266        for row in res:
267            data = {}
268            data['id']                      = row[0]
269            t_module = views.module.find_module(row[1], True)
270            module = {}
271            module['id']                    = t_module[0]['id']
272            module['fullname']              = t_module[0]['fullname']
273            module['displayname']           = t_module[0]['displayname']
274            module['shortname']             = t_module[0]['shortname']
275            data['module']                  = module
276            data['createdon']               = row[2]
277            data['updatedon']               = row[3]
278            datas.append(data)
279        cursor.close()
280        conn.close()
281        
282        # 3. 'May the Force be with you, young master'.
283        if (not internal_call): 
284            return(modules.utils.build_response_json(request.path, 200, datas)) 
285        else:
286            return(datas)
287
288
289"""
290[Summary]: Finds a dependency of a module, taking as arguments the id of the current module and the id of the module that it depends on.
291[Returns]: Response result.
292"""
293@app.route('/api/dependency/module/<module_id>/depends/<depends_on_module_id>', methods=['GET'])
294def find_dependency_of_module_2(module_id, depends_on_module_id, internal_call=False):
295    if (not internal_call):
296        if request.method != 'GET': return
297
298    # 1. Check if the user has permissions to access this resource
299    if (not internal_call): views.user.isAuthenticated(request)
300
301    # 2. Let's get the answeers for the question from the database.
302    try:
303        conn    = mysql.connect()
304        cursor  = conn.cursor()
305        print("--->" + "SELECT dependency_id, module_id, depends_module_id, createdOn, updatedOn FROM view_module_dependency WHERE module_ID=%s AND depends_module_id=%s", (module_id, depends_on_module_id))
306        cursor.execute("SELECT dependency_id, module_id, depends_module_id, createdOn, updatedOn FROM view_module_dependency WHERE module_ID=%s AND depends_module_id=%s", (module_id, depends_on_module_id))
307        res = cursor.fetchall()
308    except Exception as e:
309        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
310    
311    # 2.2. Check for empty results 
312    if (len(res) == 0):
313        cursor.close()
314        conn.close()
315        if (not internal_call):
316            return(modules.utils.build_response_json(request.path, 404)) 
317        else:
318            return None
319    else:
320        data = {}
321        for row in res:
322            data['id']                      = row[0]
323            data['module_id']               = row[1]
324            data['depends_on_module_id']    = row[2]
325            data['createdon']               = row[2]
326            data['updatedon']               = row[3]
327        cursor.close()
328        conn.close()
329        
330        # 3. 'May the Force be with you, young master'.
331        if (not internal_call): 
332            return(modules.utils.build_response_json(request.path, 200, data)) 
333        else:
334            return(data)
335
336
337"""
338[Summary]: Delete a dependency by id.
339[Returns]: Returns a success or error response
340"""
341@app.route('/api/dependency/<dependency_id>', methods=["DELETE"])
342def delete_dependency(dependency_id, internal_call=False):
343    if (not internal_call):
344        if request.method != 'DELETE': return
345
346    # Check if the user has permissions to access this resource
347    if (not internal_call): views.user.isAuthenticated(request)
348
349    # Connect to the database and delete the resource
350    try:
351        conn    = mysql.connect()
352        cursor  = conn.cursor()
353        cursor.execute("DELETE FROM dependency WHERE ID=%s", dependency_id)
354        conn.commit()
355    except Exception as e:
356        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
357    finally:
358        cursor.close()
359        conn.close()
360
361    # The Delete request was a success, the user 'took the blue pill'.
362    if (not internal_call):
363        return (modules.utils.build_response_json(request.path, 200))
364    else:
365        return(True)

Get Dependencies

GET /api/dependencies
Synopsis

Get dependencies of a module.

Request Headers
Response Headers
Response JSON Object
  • id (int) – Id of the dependency.

  • module_id (int) – Id of the module.

  • depends_on (int) – Id of the module that the module_id depends on.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/dependencies":{
      "content":[
         {
            "id":1,
            "module_id":1,
            "depends_on":2,
            "createdon":"Sat, 20 Nov 2021 13:00:50 GMT",
            "updatedon":"Sat, 20 Nov 2021 13:00:50 GMT"
         }
      ],
      "status":200
   }
}

Note

The depends_on parameter describes the id of the module that the current module_id depends on.


GET /api/dependency/(int: id)
Synopsis

Get dependency identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the dependency.

Response JSON Object
  • id (int) – Id of the dependency.

  • module_id (int) – Id of the module.

  • depends_on (int) – Id of the module that the module_id depends on.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/dependency/1":{
      "content":[
         {
            "id":1,
            "module_id":1,
            "depends_on":2,
            "createdon":"Sat, 20 Nov 2021 13:00:50 GMT",
            "updatedon":"Sat, 20 Nov 2021 13:00:50 GMT"
         }
      ],
      "status":200
   }
}

Note

The depends_on parameter describes the id of the module that the current module_id depends on.


GET /api/dependency/module/(int: id)
Synopsis

Get dependencies of a module identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the module.

Response JSON Object
  • id (int) – Id of the dependency and dependecy module.

  • module (object) – Information of the module that module id depends on.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/dependency/module/2":{
      "content":[
         {
            "id":1,
            "module":{
               "id":1,
               "shortname":"SRE",
               "fullname":"Security Requirements Elicitation",
               "displayname":"Security Requirements"
            },
            "createdon":"Wed, 17 Nov 2021 14:14:26 GMT",
            "updatedon":"Wed, 17 Nov 2021 14:14:26 GMT"
         }
      ],
      "status":200
   }
}

GET /api/dependency/module/(int: id)/depends/(int: id)
Synopsis

Get dependency information of a module id that depends on module id.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the module or id of the module it depends on.

Response JSON Object
  • id (int) – Id of the dependency.

  • module_id (int) – Id of the module.

  • depends_on_module_id (int) – Id of the module that the module_id depends on.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/dependency/module/2/depends/1":{
      "id":1,
      "module_id":2,
      "depends_on_module_id":1,
      "createdon":"Sat, 20 Nov 2021 13:00:50 GMT",
      "updatedon":"Sat, 20 Nov 2021 13:00:50 GMT",
      "status":200
   }
}

Add Dependency

POST /api/dependency
Synopsis

Add a new dependency.

Request Headers
Response Headers
Request JSON Object
  • id (int) – Id of the dependency.

  • module_id (int) – Id of the module.

  • depends_on (int) – Id of the module that the module_id depends on.

Response JSON Object
  • id (int) – The id of new dependency.

  • status (int) – Status code.

Status Codes

Example Request

{"module_id":1, "depends_on":2}

Example Response

{"/api/dependency":{"id":2, "status":200}}

Edit Dependency

PUT /api/dependency
Synopsis

Updated a dependency identified by id.

Request Headers
Response Headers
Request JSON Object
  • id (int) – Id of the dependency.

  • module_id (int) – Id of the module.

  • depends_on (int) – Id of the module that the module_id depends on.

  • depends_on_p (int) – Id of the module that the module_id previously depended on.

Response JSON Object
  • id (int) – The id of new dependency.

  • status (int) – Status code.

Status Codes

Example Request

{"id":2, "module_id":1, "depends_on":3, "p_depends_on":2}

Example Response

{"/api/dependency":{"id":3, "status":200}}

Remove Dependency

DELETE /api/dependency/(int: id)
Synopsis

Remove a dependency identified by id.

Request Headers
Response Headers
Form Parameters
  • id – The id of dependency to remove.

Response JSON Object
  • status (int) – Status code.

Status Codes

Example Response

{"/api/dependency":{"status":200}}

Questions Services API

This section includes details concerning services developed for the question entity implemented in
question.py.
  1"""
  2// ---------------------------------------------------------------------------
  3//
  4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
  5//
  6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
  7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
  8//
  9//  This program is free software: you can redistribute it and/or modify
 10//  it under the terms of the GNU General Public License as published by
 11//  the Free Software Foundation, either version 3 of the License, or
 12//  (at your option) any later version.
 13//
 14//  This program is distributed in the hope that it will be useful,
 15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
 16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17//  GNU General Public License for more details.
 18//
 19//  You should have received a copy of the GNU General Public License
 20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 21// 
 22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
 23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
 24//  POCI-01-0145-FEDER-030657) 
 25// ---------------------------------------------------------------------------
 26"""
 27from api import app, mysql
 28from flask import request
 29import modules.error_handlers, modules.utils # SAM's modules
 30import views.user, views.answer, views.module # SAM's views
 31
 32"""
 33[Summary]: Adds a new question to the database.
 34[Returns]: Response result.
 35"""
 36@app.route('/api/question', methods=['POST'])
 37def add_question():
 38    DEBUG=True
 39    if request.method != 'POST': return
 40    # Check if the user has permissions to access this resource
 41    views.user.isAuthenticated(request)
 42
 43    json_data = request.get_json()
 44    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
 45    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
 46
 47    # Validate if the necessary data is on the provided JSON 
 48    if (not modules.utils.valid_json(json_data, {"content", "description"})):
 49        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
 50
 51    content     = json_data['content']
 52    description = json_data['description']
 53    createdon   = "createdon" in json_data and json_data['createdon'] or None
 54    updatedon   = "updatedon" in json_data and json_data['updatedon'] or None
 55    
 56    # Build the SQL instruction using our handy function to build sql instructions.
 57    values = (content, description, createdon, updatedon)
 58    sql, values = modules.utils.build_sql_instruction("INSERT INTO question", ["content", "description", createdon and "createdon" or None, updatedon and "updatedon" or None], values)
 59    if (DEBUG): print("[SAM-API]: [POST]/api/question - " + sql)
 60
 61    # Add
 62    n_id = modules.utils.db_execute_update_insert(mysql, sql, values)
 63    if (n_id is None):
 64        return(modules.utils.build_response_json(request.path, 400))  
 65    else:
 66        return(modules.utils.build_response_json(request.path, 200, {"id": n_id}))  
 67
 68
 69"""
 70[Summary]: Delete a question
 71[Returns]: Returns a success or error response
 72"""
 73@app.route('/api/question/<ID>', methods=["DELETE"])
 74def delete_question(ID, internal_call=False):
 75    if (not internal_call):
 76        if request.method != 'DELETE': return
 77    
 78    # 1. Check if the user has permissions to access this resource
 79    if (not internal_call): views.user.isAuthenticated(request)
 80
 81    # 2. Connect to the database and delete the resource
 82    try:
 83        conn    = mysql.connect()
 84        cursor  = conn.cursor()
 85        cursor.execute("DELETE FROM question WHERE ID=%s", ID)
 86        conn.commit()
 87    except Exception as e:
 88        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
 89    finally:
 90        cursor.close()
 91        conn.close()
 92
 93    # 3. The Delete request was a success, the user 'took the blue pill'.
 94    if (not internal_call):
 95        return (modules.utils.build_response_json(request.path, 200))
 96    else:
 97        return(True)
 98
 99"""
100[Summary]: Updates a question.
101[Returns]: Response result.
102"""
103@app.route('/api/question', methods=['PUT'])
104def update_question():
105    DEBUG=True
106    if request.method != 'PUT': return
107    # Check if the user has permissions to access this resource
108    views.user.isAuthenticated(request)
109
110    json_data = request.get_json()
111    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
112    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
113
114    # Validate if the necessary data is on the provided JSON 
115    if (not modules.utils.valid_json(json_data, {"id"})):
116        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
117
118    content     = "content"     in json_data and json_data['content'] or None
119    description = "description" in json_data and json_data['description'] or None
120    createdon   = "createdon"   in json_data and json_data['createdon'] or None
121    updatedon   = "updatedon"   in json_data and json_data['updatedon'] or None
122
123    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
124    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
125
126    # Build the SQL instruction using our handy function to build sql instructions.
127    values  = (content, description, createdon, updatedon)
128    columns = [content and "content" or None, description and "description" or None, createdon and "createdon" or None, updatedon and "updatedOn" or None]
129    where   = "WHERE id="+str(json_data['id'])
130    # Check if there is anything to update (i.e. frontend developer has not sent any values to update).
131    if (len(values) == 0): return(modules.utils.build_response_json(request.path, 200))   
132
133    sql, values = modules.utils.build_sql_instruction("UPDATE question", columns, values, where)
134    if (DEBUG): print("[SAM-API]: [PUT]/api/question - " + sql + " " + str(values))
135
136    # Update Recommendation
137    modules.utils.db_execute_update_insert(mysql, sql, values)
138
139    return(modules.utils.build_response_json(request.path, 200))   
140
141"""
142[Summary]: Get Questions.
143[Returns]: Response result.
144"""
145@app.route('/api/questions')
146def get_questions():
147    if request.method != 'GET': return
148
149    # 1. Check if the user has permissions to access this resource
150    views.user.isAuthenticated(request)
151
152    # 2. Let's get the answeers for the question from the database.
153    try:
154        conn    = mysql.connect()
155        cursor  = conn.cursor()
156        cursor.execute("SELECT id, content, description, createdOn, updatedOn FROM question")
157        res = cursor.fetchall()
158    except Exception as e:
159        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
160    
161    # 2.2. Check for empty results 
162    if (len(res) == 0):
163        cursor.close()
164        conn.close()
165        return(modules.utils.build_response_json(request.path, 404))    
166    else:
167        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
168        for row in res:
169            data = {}
170            data['id']      = row[0]
171            data['content'] = row[1]
172            data['description'] = row[2]
173            data['type']    = 'question'
174            data['modules'] = find_modules_of_question(data['id'])
175            data['createdon']   = row[3]
176            data['updatedon']   = row[4]
177            datas.append(data)
178        cursor.close()
179        conn.close()
180        # 3. 'May the Force be with you, young master'.
181        return(modules.utils.build_response_json(request.path, 200, datas)) 
182
183"""
184[Summary]: Finds the list of modules linked to a question
185[Returns]: A list of modules or an empty array if None are found.
186"""
187def find_modules_of_question(question_id):
188    try:
189        conn    = mysql.connect()
190        cursor  = conn.cursor()
191        cursor.execute("SELECT module_id FROM view_module_question WHERE question_id=%s", question_id)
192        res = cursor.fetchall()
193    except Exception as e:
194        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
195    
196    # Check for empty results 
197    if (len(res) == 0):
198        cursor.close()
199        # Check if this question is a child of another question, triggered by an answer, were the parent belongs to a module.
200        # We need to do this because sub-questions, triggered by an answer, are not directly linked to a module. 
201        # In order to find the module that the sub-question belongs, we need to find its parent.
202        try:
203            conn    = mysql.connect()
204            cursor  = conn.cursor()
205            cursor.execute("SELECT parent FROM question_has_child WHERE child=%s", question_id)
206            res = cursor.fetchall()
207        except Exception as e:
208            raise modules.error_handlers.BadRequest(request.path, str(e), 500)
209        
210        if (len(res) == 0):
211            cursor.close()
212            conn.close()
213            return([]) 
214        else:
215            l_modules = []
216            for row in res:
217                modules = find_modules_of_question(row[0])
218                l_modules.append(modules)
219            #
220            f_list_modules = []
221            for modules in l_modules:
222                for module in modules:
223                    if module not in f_list_modules:
224                        f_list_modules.append(module)
225            return(f_list_modules)
226            cursor.close()
227            conn.close()
228    else:
229        modules = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
230        for row in res:
231            module = views.module.find_module(row[0], True)[0]
232            del module['tree']
233            del module['dependencies']
234            del module['recommendations']
235            modules.append(module)
236        cursor.close()
237        conn.close()
238       
239        # 'May the Force be with you, young master'.
240        return(modules)
241
242"""
243[Summary]: Finds Question.
244[Returns]: Response result.
245"""
246@app.route('/api/question/<ID>', methods=['GET'])
247def find_question(ID, internal_call=False):
248    if (not internal_call):
249        if request.method != 'GET': return
250
251    # 1. Check if the user has permissions to access this resource
252    if (not internal_call):
253        views.user.isAuthenticated(request)
254
255    # 2. Let's get the answeers for the question from the database.
256    try:
257        conn    = mysql.connect()
258        cursor  = conn.cursor()
259        cursor.execute("SELECT ID as question_id, content, description, createdOn, updatedOn FROM question WHERE ID=%s", ID)
260        res = cursor.fetchall()
261    except Exception as e:
262        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
263    
264    # 2.2. Check for empty results 
265    if (len(res) == 0):
266        cursor.close()
267        conn.close()
268        return(modules.utils.build_response_json(request.path, 404))    
269    else:
270        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
271        for row in res:
272            data = {}
273            data['id']      = row[0]
274            data['content'] = row[1]
275            data['type']    = 'question'
276            datas.append(data)
277        cursor.close()
278        conn.close()
279        
280        # 3. 'May the Force be with you, young master'.
281        if (not internal_call): 
282            return(modules.utils.build_response_json(request.path, 200, datas)) 
283        else:
284            return(datas)
285
286"""
287[Summary]: Finds Answers of a Question by question ID ans answerID- [Question_Answer] Table.
288[Returns]: Response result.
289"""
290@app.route('/api/question/<question_id>/answer/<answer_id>', methods=['GET'])
291def find_question_answers_2(question_id, answer_id, internal_call=False):
292    if (request.method != 'GET' and not internal_call): return
293
294    
295    # 1. Check if the user has permissions to access this resource
296    if (not internal_call): views.user.isAuthenticated(request)
297
298    # 2. Let's get the answeers for the question from the database.
299    try:
300        conn    = mysql.connect()
301        cursor  = conn.cursor()
302        cursor.execute("SELECT question_answer_id, question_id, question, answer_id, answer FROM view_question_answer where question_id=%s and answer_id=%s", (question_id, answer_id))
303        res = cursor.fetchall()
304    except Exception as e:
305        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
306    
307    # 2.2. Check for empty results 
308    if (len(res) == 0):
309        cursor.close()
310        conn.close()
311        if (not internal_call):
312            return(modules.utils.build_response_json(request.path, 404))
313        else:
314            return(None)
315    else:
316        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
317        for row in res:
318            data = {}
319            data['question_answer_id']  = row[0]
320            data['question_id']         = row[1]
321            data['question_content']    = row[2]
322            data['answer_id']           = row[3]
323            data['answer_content']      = row[4]
324            datas.append(data)
325        cursor.close()
326        conn.close()
327        
328        # 3. 'May the Force be with you, young master'.
329        if (not internal_call):
330            return(modules.utils.build_response_json(request.path, 200, datas))
331        else:
332            return(datas)
333
334"""
335[Summary]: Finds Answers of a Question - [Question_Answer] Table.
336[Returns]: Response result.
337"""
338@app.route('/api/question/<ID>/answers', methods=['GET'])
339def find_question_answers(ID):
340    if request.method != 'GET': return
341
342    # 1. Check if the user has permissions to access this resource
343    views.user.isAuthenticated(request)
344
345    # 2. Let's get the answeers for the question from the database.
346    try:
347        conn    = mysql.connect()
348        cursor  = conn.cursor()
349        cursor.execute("SELECT question_answer_id, question_id, question, answer_id, answer FROM view_question_answer where question_id=%s", ID)
350        res = cursor.fetchall()
351    except Exception as e:
352        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
353    
354    # 2.2. Check for empty results 
355    if (len(res) == 0):
356        cursor.close()
357        conn.close()
358        return(modules.utils.build_response_json(request.path, 404))    
359    else:
360        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
361        for row in res:
362            data = {}
363            data['question_answer_id']  = row[0]
364            data['question_id']         = row[1]
365            data['question_content']    = row[2]
366            data['answer_id']           = row[3]
367            data['answer_content']      = row[4]
368            datas.append(data)
369        cursor.close()
370        conn.close()
371        # 3. 'May the Force be with you, young master'.
372        return(modules.utils.build_response_json(request.path, 200, datas))    
373
374
375"""
376[Summary]: Gets Answers of a Questions - [Question_Answer] Table.
377[Returns]: Response result.
378"""
379@app.route('/api/questions/answers', methods=['GET'])
380def get_questions_answers():
381   
382    if request.method != 'GET': return
383
384    # 1. Check if the user has permissions to access this resource
385    views.user.isAuthenticated(request)
386
387    # 2. Let's get the questions from the database 
388    try:
389        conn    = mysql.connect()
390        cursor  = conn.cursor()
391        cursor.execute("SELECT question_answer_id, question_id, question, answer_id, answer FROM view_question_answer")
392        res = cursor.fetchall()
393    except Exception as e:
394        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
395    
396    # 2.2. Check for empty results 
397    if (len(res) == 0):
398        cursor.close()
399        conn.close()
400        return(modules.utils.build_response_json(request.path, 404))    
401    else:
402        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
403        for row in res:
404            data = {}
405            data['question_answer_id']  = row[0]
406            data['question_id']         = row[1]
407            data['question_content']    = row[2]
408            data['answer_id']           = row[3]
409            data['answer_content']      = row[4]
410            datas.append(data)
411    
412    cursor.close()
413    conn.close()
414    # 3. 'May the Force be with you, young master'.
415    return(modules.utils.build_response_json(request.path, 200, datas))    

Get Questions

GET /api/questions
Synopsis

Get all questions.

Request Headers
Response Headers
Response JSON Object
  • id (int) – Id of the current question.

  • content (string) – The question.

  • description (string) – Question description.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – status code.

Status Codes

Example Response

{
   "/api/questions":{
      "content":[
         {
            "id":3,
            "content":"What is the name of ... ?",
            "description":"Test question description",
            "modules":[],
            "createdon":"Sun, 20 Sep 2020 09:00:00 GMT",
            "updatedon":"Sun, 20 Sep 2020 09:00:00 GMT"
         }
      ],
      "status":200
   }
}

GET /api/question/(int: id)
Synopsis

Get a question identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – id of the question.

Response JSON Object
  • id (int) – Id of the question.

  • content (string) – The question.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/question/1":{
      "content":[
         {
            "id":1,
            "content":"What is the domain of your IoT system ?"
         }
      ],
      "status":200
   }
}

Get Questions Answers

GET /api/questions/answers
Synopsis

Get the list of answers mapped to questions

Request Headers
Response Headers
Response JSON Object
  • question_id (int) – The id of the question.

  • question_content (string) – The question content.

  • answer_id (int) – The id of que answer mapped to that question.

  • answer_content (string) – The answer content.

  • question_answer_id – The relation id between question and answer.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/questions/answers":{
      "content":[
         {
            "question_id":1
            "question_content":"What is the domain of your IoT system ?",
            "answer_id":1,
            "answer_content":"Smart home",
            "question_answer_id":1,
         },
         {
            "question_id":1
            "question_content":"What is the domain of your IoT system ?",
            "answer_id":2,
            "answer_content":"Smart Healthcare",
            "question_answer_id":2,
         }
      ],
      "status":200
   }
}

GET /api/question/(int: id)/answers
Synopsis

Get the list of answers mapped to question identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – id of the question.

Response JSON Object
  • question_id (int) – The id of the question.

  • question_content (string) – The question content.

  • answer_id (int) – The id of que answer mapped to that question.

  • answer_content (string) – The answer content.

  • question_answer_id – The relation id between question and answer.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/question/1/answers":{
      "content":[
         {
            "question_id":1,
            "question_content":"What is the domain of your IoT system ?",
            "answer_id":1,
            "answer_content":"Smart home",
            "question_answer_id":1
         },
         {
            "question_id":1,
            "question_content":"What is the domain of your IoT system ?",
            "answer_id":2,
            "answer_content":"Smart Healthcare",
            "question_answer_id":2
         }
      ],
      "status":200
   }
}

Add Question

POST /api/question
Synopsis

Add a new question.

Request Headers
Response Headers
Request JSON Object
  • content (string) – The question content.

  • description (string) – The question description.

Response JSON Object
  • id (int) – The id of the new question.

  • status (string) – Status code.

Status Codes

Example Request

{
   "content":"Test Question",
   "description":"Test question description",
}

Example Response

{"/api/question":{"id":1, "status":200}}

Edit Question

PUT /api/question
Synopsis

Update a question identified by id.

Request Headers
Response Headers
Request JSON Object
  • id (int) – The id of the question to update.

  • content (string) – The question content.

  • description (string) – The question description.

Response JSON Object
  • status (string) – Status code.

Status Codes

Example Request

{
   "id": 1,
   "content":"Test Question Updated",
   "description":"Test question description updated",
}

Example Response

{"/api/question":{"status":200}}

Remove Question

DELETE /api/question/(int: id)
Synopsis

Removes a question identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the question to remove.

Response JSON Object
  • status (string) – Status code.

Status Codes

Example Response

{"/api/question":{"status":200}}

Answers Services API

This section includes details concerning services developed for the answer entity implemented in
answer.py.
  1"""
  2// ---------------------------------------------------------------------------
  3//
  4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
  5//
  6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
  7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
  8//
  9//  This program is free software: you can redistribute it and/or modify
 10//  it under the terms of the GNU General Public License as published by
 11//  the Free Software Foundation, either version 3 of the License, or
 12//  (at your option) any later version.
 13//
 14//  This program is distributed in the hope that it will be useful,
 15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
 16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17//  GNU General Public License for more details.
 18//
 19//  You should have received a copy of the GNU General Public License
 20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 21// 
 22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
 23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
 24//  POCI-01-0145-FEDER-030657) 
 25// ---------------------------------------------------------------------------
 26"""
 27from api import app, mysql
 28from flask import request
 29import modules.error_handlers, modules.utils # SAM's modules
 30import views.user, views.question # SAM's views
 31
 32"""
 33[Summary]: Adds a new question to the database.
 34[Returns]: Response result.
 35"""
 36@app.route('/api/answer', methods=['POST'])
 37def add_answer():
 38    DEBUG=True
 39    if request.method != 'POST': return
 40    # Check if the user has permissions to access this resource
 41    views.user.isAuthenticated(request)
 42
 43    json_data = request.get_json()
 44    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
 45    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
 46
 47    # Validate if the necessary data is on the provided JSON 
 48    if (not modules.utils.valid_json(json_data, {"content", "description"})):
 49        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
 50
 51    content     = json_data['content']
 52    description = json_data['description']
 53    createdon   = "createdon" in json_data and json_data['createdon'] or None
 54    updatedon   = "updatedon" in json_data and json_data['updatedon'] or None
 55    
 56    # Build the SQL instruction using our handy function to build sql instructions.
 57    values = (content, description, createdon, updatedon)
 58    sql, values = modules.utils.build_sql_instruction("INSERT INTO answer", ["content", "description", createdon and "createdon" or None, updatedon and "updatedon" or None], values)
 59    if (DEBUG): print("[SAM-API]: [POST]/api/answer - " + sql)
 60
 61    # Add
 62    n_id = modules.utils.db_execute_update_insert(mysql, sql, values)
 63    if (n_id is None):
 64        return(modules.utils.build_response_json(request.path, 400))  
 65    else:
 66        return(modules.utils.build_response_json(request.path, 200, {"id": n_id}))
 67
 68
 69"""
 70[Summary]: Delete an answer.
 71[Returns]: Returns a success or error response
 72"""
 73@app.route('/api/answer/<ID>', methods=["DELETE"])
 74def delete_answer(ID, internal_call=False):
 75    if (not internal_call):
 76        if request.method != 'DELETE': return
 77    
 78    # 1. Check if the user has permissions to access this resource
 79    if (not internal_call): views.user.isAuthenticated(request)
 80
 81    # 2. Connect to the database and delete the resource
 82    try:
 83        conn    = mysql.connect()
 84        cursor  = conn.cursor()
 85        cursor.execute("DELETE FROM answer WHERE ID=%s", ID)
 86        conn.commit()
 87    except Exception as e:
 88        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
 89    finally:
 90        cursor.close()
 91        conn.close()
 92
 93    # 3. The Delete request was a success, the user 'took the blue pill'.
 94    if (not internal_call):
 95        return (modules.utils.build_response_json(request.path, 200))
 96    else:
 97        return(True)
 98
 99"""
100[Summary]: Updates a question.
101[Returns]: Response result.
102"""
103@app.route('/api/answer', methods=['PUT'])
104def update_answer():
105    DEBUG=True
106    if request.method != 'PUT': return
107    # Check if the user has permissions to access this resource
108    views.user.isAuthenticated(request)
109
110    json_data = request.get_json()
111    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
112    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
113
114    # Validate if the necessary data is on the provided JSON 
115    if (not modules.utils.valid_json(json_data, {"id"})):
116        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
117
118    content     = "content"     in json_data and json_data['content'] or None
119    description = "description" in json_data and json_data['description'] or None
120    createdon   = "createdon"   in json_data and json_data['createdon'] or None
121    updatedon   = "updatedon"   in json_data and json_data['updatedon'] or None
122
123    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
124    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
125
126    # Build the SQL instruction using our handy function to build sql instructions.
127    values  = (content, description, createdon, updatedon)
128    columns = [content and "content" or None, description and "description" or None, createdon and "createdon" or None, updatedon and "updatedOn" or None]
129    where   = "WHERE id="+str(json_data['id'])
130    # Check if there is anything to update (i.e. frontend developer has not sent any values to update).
131    if (len(values) == 0): return(modules.utils.build_response_json(request.path, 200))   
132
133    sql, values = modules.utils.build_sql_instruction("UPDATE answer", columns, values, where)
134    if (DEBUG): print("[SAM-API]: [PUT]/api/answer - " + sql + " " + str(values))
135
136    # Update Recommendation
137    modules.utils.db_execute_update_insert(mysql, sql, values)
138
139    return(modules.utils.build_response_json(request.path, 200))   
140
141
142"""
143[Summary]: Get Answers.
144[Returns]: Response result.
145"""
146@app.route('/api/answers')
147def get_answers():
148    if request.method != 'GET': return
149
150    # 1. Check if the user has permissions to access this resource
151    views.user.isAuthenticated(request)
152
153    # 2. Let's get the answeers for the question from the database.
154    try:
155        conn    = mysql.connect()
156        cursor  = conn.cursor()
157        cursor.execute("SELECT ID, content, description, createdon, updatedon FROM answer")
158        res = cursor.fetchall()
159    except Exception as e:
160        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
161    
162    # 2.2. Check for empty results 
163    if (len(res) == 0):
164        cursor.close()
165        conn.close()
166        return(modules.utils.build_response_json(request.path, 404))    
167    else:
168        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
169        for row in res:
170            data = {}
171            data['id']          = row[0]
172            data['content']     = row[1]
173            data['description'] = row[2]
174            data['questions']   = find_questions_of_answer(row[0])
175            data['createdon']   = row[3]
176            data['updatedon']   = row[4]
177            datas.append(data)
178        cursor.close()
179        conn.close()
180        # 3. 'May the Force be with you, young master'.
181        return(modules.utils.build_response_json(request.path, 200, datas))
182
183"""
184[Summary]: Finds the list of questions linked to an answer.
185[Returns]: A list of questions or an empty array if None are found.
186"""
187def find_questions_of_answer(answer_id):
188    try:
189        conn    = mysql.connect()
190        cursor  = conn.cursor()
191        cursor.execute("SELECT questionID FROM question_answer WHERE answerID=%s", answer_id)
192        res = cursor.fetchall()
193    except Exception as e:
194        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
195    
196    # Check for empty results 
197    if (len(res) == 0):
198        cursor.close()
199        conn.close()
200        return([]) 
201       
202    else:
203        questions = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
204        for row in res:
205            question = views.question.find_question(row[0], True)[0]
206            questions.append(question)
207        cursor.close()
208        conn.close()
209       
210        # 'May the Force be with you, young master'.
211        return(questions)
212
213
214"""
215[Summary]: Finds Answer.
216[Returns]: Response result.
217"""
218@app.route('/api/answer/<ID>', methods=['GET'])
219def find_answer(ID, internal_call=False):
220    if request.method != 'GET': return
221
222    # 1. Check if the user has permissions to access this resource
223    if (not internal_call): views.user.isAuthenticated(request)
224
225    # 2. Let's get the answeers for the question from the database.
226    try:
227        conn    = mysql.connect()
228        cursor  = conn.cursor()
229        cursor.execute("SELECT ID, content, description, createdon, updatedon FROM answer WHERE ID=%s", ID)
230        res = cursor.fetchall()
231    except Exception as e:
232        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
233    
234    # 2.2. Check for empty results 
235    if (len(res) == 0):
236        cursor.close()
237        conn.close()
238        return(modules.utils.build_response_json(request.path, 404))    
239    else:
240        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
241        for row in res:
242            data = {}
243            data['id']          = row[0]
244            data['content']     = row[1]
245            data['description'] = row[2]
246            data['createdon']   = row[3]
247            data['updatedon']   = row[4]
248            datas.append(data)
249        cursor.close()
250        conn.close()
251        # 3. 'May the Force be with you, young master'.
252        return(modules.utils.build_response_json(request.path, 200, datas)) 

Get Answers

GET /api/answers
Synopsis

Get the list of answers mapped to questions.

Request Headers
Response Headers
Response JSON Object
  • id (int) – The id of the answer or question mapped to the current answer.

  • content (string) – The answer or question content.

  • description (string) – Description of the answer.

  • questions (array) – Array of questions mapped to the current answer.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/answers":{
      "content":[
         {
            "id":1,
            "content":"Smart home",
            "description":"Smart home description",
            "questions":[
               {
                  "id":1,
                  "content":"What is the domain of your IoT system ?"
               }
            ],
            "createdon":"Fri, 19 Nov 2021 15:29:18 GMT",
            "updatedon":"Fri, 19 Nov 2021 15:29:18 GMT"
         },
         {
            "id":2,
            "content":"Smart Healthcare",
            "description":"Smart healthcare description",
            "questions":[
               {
                  "id":1,
                  "content":"What is the domain of your IoT system ?"
               }
            ],
            "createdon":"Fri, 19 Nov 2021 15:29:18 GMT",
            "updatedon":"Fri, 19 Nov 2021 15:29:18 GMT"
         }
      ],
      "status":200
   }
}

GET /api/answers/(int: id)
Synopsis

Get an answer identify by id.

Request Headers
Response Headers
Response JSON Object
  • id (int) – The id of the answer.

  • content (string) – The answer content.

  • description (string) – Description of the answer.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/answer/1":{
      "content":[
         {
            "id":1,
            "content":"Smart home",
            "description":"Smart home description",
            "createdon":"Fri, 19 Nov 2021 15:29:18 GMT",
            "updatedon":"Fri, 19 Nov 2021 15:29:18 GMT"
         }
      ],
      "status":200
   }
}

Add Answer

POST /api/answer
Synopsis

Add a new answer.

Request Headers
Response Headers
Request JSON Object
  • content (string) – The answer content.

  • description (string) – The answer description.

Response JSON Object
  • id (int) – The id of the new answer.

  • status (string) – Status code.

Status Codes

Example Request

{
        "content":"Test Answer",
        "description":"Test answer description"
}

Example Response

{"/api/answer":{"id":4, "status":200}}

Edit Answer

PUT /api/answer
Synopsis

Update an answer identified by id.

Request Headers
Response Headers
Request JSON Object
  • id (int) – The id of the answer to update.

  • content (string) – The answer content.

  • description (string) – The answer description.

Response JSON Object
  • status (string) – Status code.

Status Codes

Example Request

{
   "id":1,
   "content":"Test Answer Updated",
   "description":"Test answer description updated"
}

Example Response

{"/api/answer":{"status":200}}

Remove Answer

DELETE /api/answer/(int: id)
Synopsis

Removes a answer identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the answer to remove.

Response JSON Object
  • status (string) – Status code.

Status Codes

Example Response

{"/api/answer":{"status":200}}

Recommendations Services API

This section includes details concerning services developed for the recommendation entity implemented in
recommendation.py.
  1"""
  2// ---------------------------------------------------------------------------
  3//
  4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
  5//
  6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
  7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
  8//
  9//  This program is free software: you can redistribute it and/or modify
 10//  it under the terms of the GNU General Public License as published by
 11//  the Free Software Foundation, either version 3 of the License, or
 12//  (at your option) any later version.
 13//
 14//  This program is distributed in the hope that it will be useful,
 15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
 16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17//  GNU General Public License for more details.
 18//
 19//  You should have received a copy of the GNU General Public License
 20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 21// 
 22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
 23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
 24//  POCI-01-0145-FEDER-030657) 
 25// ---------------------------------------------------------------------------
 26"""
 27from api import app, mysql
 28from flask import request
 29import os
 30import modules.error_handlers, modules.utils # SAM's modules
 31import views.user, views.module # SAM's views
 32
 33
 34"""
 35[Summary]: Adds a new recommendation to the database.
 36[Returns]: Response result.
 37"""
 38@app.route('/api/recommendation', methods=['POST'])
 39def add_recommendation(internal_json=None):
 40    DEBUG=False
 41    if (internal_json is None):
 42        if (request.method != 'POST'): return
 43    
 44    # Check if the user has permissions to access this resource
 45    if (internal_json is None): views.user.isAuthenticated(request)
 46   
 47    if (internal_json is None):
 48        json_data = request.get_json()
 49    else:
 50        json_data = internal_json
 51
 52    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
 53    if (json_data is None): 
 54        if (internal_json is None):
 55            return(modules.utils.build_response_json(request.path, 400)) 
 56        else:
 57            return(None)
 58    
 59    # Validate if the necessary data is on the provided JSON
 60    # Check if the recommendation [id] is null. If not null, it means the recommendation was previsoulyed added and we just need to add the question_answers mapping to table [recommendation_question_answer].
 61    if (json_data['id'] is None):
 62        if (not modules.utils.valid_json(json_data, {"content"})):
 63            raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
 64        
 65        content         = json_data['content']
 66        description     = "description" in json_data and json_data['description']     or None
 67        guide           = "guide"     in json_data and json_data['guide']     or None
 68        createdon       = "createdon" in json_data and json_data['createdon'] or None
 69        updatedon       = "updatedon" in json_data and json_data['updatedon'] or None
 70    
 71        # Build the SQL instruction using our handy function to build sql instructions.
 72        values = (content, description, guide, createdon, updatedon)
 73        sql, values = modules.utils.build_sql_instruction("INSERT INTO recommendation", ["content", description and "description" or None, guide and "guidefilename" or None, createdon and "createdon" or None, updatedon and "updatedon" or None], values)
 74        if (DEBUG): print("[SAM-API]: [POST]/recomendation - " + sql + " " + str(values))
 75
 76        # Add
 77        recommendation_id = modules.utils.db_execute_update_insert(mysql, sql, values)
 78        if (recommendation_id is None): 
 79            if (internal_json is None):
 80                return(modules.utils.build_response_json(request.path, 400))  
 81            else:
 82                return("None")
 83        
 84        # If availabe, set the final guide filename after knowing the database id of the new recommendation
 85        if guide and recommendation_id:
 86            # Get the file extension of the guide uploaded (it can be txt or md) in order to create the final name of the file.
 87            file_extension = guide[guide.rfind("."): len(guide)]
 88            final_recommendation_filename = "recommendation_" + str(recommendation_id) + file_extension
 89            sql, values = modules.utils.build_sql_instruction("UPDATE recommendation", ["guideFileName"], final_recommendation_filename, "WHERE id="+str(recommendation_id))
 90            modules.utils.db_execute_update_insert(mysql, sql, values, True)
 91        
 92    else:
 93        recommendation_id = json_data['id']
 94 
 95
 96
 97    # This recommendation is given if a question answer association is defined.
 98    # question_answer_id is a column in table "Recommendation_Question_Answer"
 99    questions_answers = "questions_answers" in json_data and json_data['questions_answers'] or None
100    if (questions_answers is None): 
101        if (internal_json is None):
102            return(modules.utils.build_response_json(request.path, 200, {"id": recommendation_id}))   
103        else:
104            return({"id": recommendation_id})
105    
106    # Build the SQL instruction using our handy function to build sql instructions.
107    for question_answer in questions_answers: 
108        question_answer_id = question_answer['id']
109        columns = ["recommendationID", "questionAnswerID"] 
110        values  = (recommendation_id, question_answer_id)
111        sql, values = modules.utils.build_sql_instruction("INSERT INTO recommendation_question_answer", columns, values)
112        if (DEBUG): print("[SAM-API]: [POST]/recomendation - " + sql + " " + str(values))
113        # Add
114        rqa_id = modules.utils.db_execute_update_insert(mysql, sql, values)
115
116    if (rqa_id is None):
117        if (internal_json is None):
118            return(modules.utils.build_response_json(request.path, 400))
119        else:
120            return(None)
121    else:
122        if (internal_json is None):
123            return(modules.utils.build_response_json(request.path, 200, {"id": recommendation_id}))   
124        else:
125            return({"id", recommendation_id})
126
127"""
128[Summary]: Delete a recommendation.
129[Returns]: Returns a success or error response
130"""
131@app.route('/api/recommendation/<recommendation_id>', methods=["DELETE"])
132def delete_recommendation(recommendation_id):
133    if request.method != 'DELETE': return
134    # 1. Check if the user has permissions to access this resource
135    views.user.isAuthenticated(request)
136    
137    # 2. If any, get the filename of the guide linked to the recommendation.
138    recommendation_guide = find_recommendation(recommendation_id, True)[0]['guide']
139
140    # 3. Connect to the database and delete the resource
141    try:
142        conn    = mysql.connect()
143        cursor  = conn.cursor()
144        cursor.execute("DELETE FROM recommendation WHERE ID=%s", recommendation_id)
145        conn.commit()
146    except Exception as e:
147        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
148    finally:
149        cursor.close()
150        conn.close()
151        # 3.1. If guide available, remove the file from the server.
152        if (recommendation_guide): 
153            try:
154                os.remove(os.path.join(views.file.UPLOAD_DIRECTORY, recommendation_guide))
155            except Exception as e:
156                pass # For some reason, the file may not exist.
157
158    # 4. The Delete request was a success, the user 'took the blue pill'.
159    return (modules.utils.build_response_json(request.path, 200))
160
161"""
162[Summary]: Updates a recommendation
163[Returns]: Response result.
164"""
165@app.route('/api/recommendation', methods=['PUT'])
166def update_recommendation():
167    DEBUG=False
168    if request.method != 'PUT': return
169    # Check if the user has permissions to access this resource
170    views.user.isAuthenticated(request)
171
172    json_data = request.get_json()
173    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
174    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
175
176    # Validate if the necessary data is on the provided JSON 
177    if (not modules.utils.valid_json(json_data, {"id"})):
178        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)    
179
180    content     = "content"     in json_data and json_data['content'] or None
181    description = "description" in json_data and json_data['description'] or None
182    guide       = "guide"       in json_data and json_data['guide']     or None
183    createdon   = "createdon"   in json_data and json_data['createdon'] or None
184    updatedon   = "updatedon"   in json_data and json_data['updatedon'] or None
185
186    # If the mimetype does not indicate JSON (application/json, see is_json()), this returns None.
187    if (json_data is None): return(modules.utils.build_response_json(request.path, 400)) 
188
189    # If availabe, set the final guide filename after knowing the database id of the new recommendation
190
191    if guide and json_data['id']:
192        # Remove previous guide.
193        previous_guide = find_recommendation(json_data['id'], True)[0]['guide']
194        if (guide != previous_guide): 
195            os.remove(os.path.join(views.file.UPLOAD_DIRECTORY, previous_guide))
196
197        # Get the file extension of the guide uploaded (it can be txt or md) in order to create the final name of the file.
198        file_extension = guide[guide.rfind("."): len(guide)]
199        final_recommendation_filename = "recommendation_" + str(json_data['id']) + file_extension
200        guide = final_recommendation_filename
201
202    # Build the SQL instruction using our handy function to build sql instructions.
203    values  = (content, description, guide, createdon, updatedon)
204    columns = [content and "content" or None, description and "description" or None, guide and "guideFileName" or None, createdon and "createdon" or None, updatedon and "updatedOn" or None]
205    where   = "WHERE id="+str(json_data['id'])
206    # Check if there is anything to update (i.e. frontend developer has not sent any values to update).
207    if (len(values) == 0): return(modules.utils.build_response_json(request.path, 200))   
208
209    sql, values = modules.utils.build_sql_instruction("UPDATE recommendation", columns, values, where)
210    if (DEBUG): print("[SAM-API]: [PUT]/recomendation - " + sql + " " + str(values))
211
212    # Update Recommendation
213    modules.utils.db_execute_update_insert(mysql, sql, values)
214
215    return(modules.utils.build_response_json(request.path, 200))   
216
217"""
218[Summary]: Get recommendations.
219[Returns]: Response result.
220"""
221@app.route('/api/recommendations', methods=['GET'])
222def get_recommendations(internal_call=False):
223    if (not internal_call): 
224        if request.method != 'GET': return
225    # 1. Check if the user has permissions to access this resource
226    if (not internal_call): 
227        views.user.isAuthenticated(request)
228
229    # 2. Let's get the set of available recommendations
230    recommendations = []
231    try:
232        conn    = mysql.connect()
233        cursor  = conn.cursor()
234        cursor.execute("SELECT ID, content, description, guideFileName, createdon, updatedon FROM recommendation")
235        res = cursor.fetchall()
236        for row in res:
237            recommendation = {}
238            recommendation['id']          = row[0]
239            recommendation['content']     = row[1]
240            recommendation['description'] = row[2]
241            recommendation['guide']       = row[3]
242            recommendation['createdOn']   = row[4]
243            recommendation['updatedOn']   = row[5]
244            recommendation['modules']     = []
245            recommendations.append(recommendation)
246    except Exception as e:
247        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
248    finally:
249
250        # 3. Let's get the info about the modules linked to each recommendation.
251        cursor.close()
252        for recommendation in recommendations:
253            try:
254                cursor  = conn.cursor()
255                cursor.execute("SELECT module_id FROM view_module_recommendations WHERE recommendation_id=%s", recommendation['id'])
256                res = cursor.fetchall()
257                for row in res:
258                    module = views.module.find_module(row[0], True)[0]
259                    # Remove unnecessary information from the object
260                    if ("tree" in module): del module['tree']
261                    if ("recommendations" in module): del module['recommendations']
262                    if ("dependencies" in module): del module['dependencies']
263                    recommendation['modules'].append(module)
264            except Exception as e:
265                raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
266            cursor.close()
267        
268        conn.close()
269        # 4. The request was a success, the user 'is in the rabbit hole'
270        if (not internal_call):
271            return(modules.utils.build_response_json(request.path, 200, recommendations))
272        else:
273            return(recommendations)
274
275"""
276[Summary]: Finds recommendation by ID.
277[Returns]: Response result.
278"""
279@app.route('/api/recommendation/<ID>', methods=['GET'])
280def find_recommendation(ID, internal_call=False):
281    if (not internal_call): 
282        if request.method != 'GET': return
283    # 1. Check if the user has permissions to access this resource
284    if (not internal_call): views.user.isAuthenticated(request)
285
286    # 2. Let's get the set of available recommendations
287    results = []
288    try:
289        conn    = mysql.connect()
290        cursor  = conn.cursor()
291        cursor.execute("SELECT ID, content, description, guideFileName, createdon, updatedon FROM recommendation WHERE ID=%s", ID)
292        res = cursor.fetchall()
293        for row in res:
294            result = {}
295            result['id']          = row[0]
296            result['content']     = row[1]
297            result['description'] = row[2]
298            result['guide']       = row[3]
299            result['createdOn']   = row[4]
300            result['updatedOn']   = row[5]
301            results.append(result)
302    except Exception as e:
303        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
304    finally:
305        cursor.close()
306        conn.close()
307        
308        # 3. The request was a success, the user 'is in the rabbit hole'
309        if (not internal_call):
310            return(modules.utils.build_response_json(request.path, 200, results)) 
311        else:
312            return(results)
313"""
314[Summary]: Finds the recommendations of question, answer association.
315[Returns]: Response result.
316"""
317@app.route('/api/recommendations/question/<question_id>/answer/<answer_id>', methods=['GET'])
318def find_recommendations_of_question_answer(question_id, answer_id, internal_call=False):
319    if request.method != 'GET': return
320    # Check if the user has permissions to access this resource
321    if (not internal_call): views.user.isAuthenticated(request)
322    
323    try:
324        conn    = mysql.connect()
325        cursor  = conn.cursor()
326        cursor.execute("SELECT recommendation_id as id, content, description, guide, createdon, updatedon FROM view_question_answer_recommendation WHERE question_id = %s AND answer_id = %s", (question_id, answer_id))
327        res = cursor.fetchall()
328    except Exception as e:
329        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
330
331    # 2.2. Check for empty results 
332    if (len(res) == 0):
333        cursor.close()
334        conn.close()
335        return(None)    
336    else:
337        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
338        for row in res:
339            data = {}
340            data['id']          = row[0]
341            data['content']     = row[1]
342            data['description'] = row[2]
343            data['guide']       = row[3] 
344            data['type']        = "recommendation"
345            data['children']    = []
346            data['createdon']   = row[4]
347            data['updatedon']   = row[5]
348            datas.append(data)
349        cursor.close()
350        conn.close()
351        # 3. 'May the Force be with you, young master'.
352        if (internal_call):
353            return(datas)
354        else:
355            return(modules.utils.build_response_json(request.path, 200, datas)) 
356
357"""
358[Summary]: Finds the recommendations of a module
359[Returns]: Response result.
360"""
361@app.route('/api/recommendations/module/<module_id>', methods=['GET'])
362def find_recommendations_of_module(module_id, internal_call=False):
363    if (not internal_call):
364        if request.method != 'GET': 
365            return
366    
367    # Check if the user has permissions to access this resource
368    if (not internal_call): views.user.isAuthenticated(request)
369    
370    try:
371        conn    = mysql.connect()
372        cursor  = conn.cursor()
373        cursor.execute("SELECT recommendation_ID, recommendation_content, createdon, updatedon FROM view_module_recommendations WHERE module_id=%s", module_id)
374        res = cursor.fetchall()
375    except Exception as e:
376        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
377
378    # Check for empty results 
379    if (len(res) == 0):
380        cursor.close()
381        conn.close()
382        return(None)    
383    else:
384        recommendations = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
385        for row in res:
386            recommendation = {}
387            recommendation['id']                  = row[0]
388            recommendation['content']             = row[1]
389            recommendation['createdon']           = row[2]
390            recommendation['updatedon']           = row[3]
391            recommendations.append(recommendation)
392    
393    # Find questions and answers associated or mapped to a particular recommendation and module
394    for recommendation in recommendations:
395        recommendation_id = recommendation['id']
396        try:
397            cursor  = conn.cursor()
398            cursor.execute("SELECT recommendation_question_answer_id, question_id, answer_id, createdon, updatedon FROM view_module_recommendations_questions_answers WHERE module_id=%s and recommendation_id=%s", (module_id, recommendation_id))
399            res = cursor.fetchall()
400        except Exception as e:
401            raise modules.error_handlers.BadRequest(request.path, str(e), 500)
402        if (len(res) != 0):
403            questions_answers = []
404            for row in res:
405                question_answer = {}
406                question_answer['id']          = row[0]
407                question_answer['question_id'] = row[1]
408                question_answer['answer_id']   = row[2]
409                question_answer['createdon']   = row[3]
410                question_answer['updatedon']   = row[4]
411                questions_answers.append(question_answer)
412            cursor.close()
413            recommendation['questions_answers'] = questions_answers
414    
415    # 'May the Force be with you, young master'.
416    conn.close()
417    if (internal_call):
418        return(recommendations)
419    else:
420        return(modules.utils.build_response_json(request.path, 200, recommendations)) 
421
422
423"""
424[Summary]: Removes the mapping between a module and its recommendations.
425[Returns]: Returns a reponse object.
426"""
427@app.route('/api/recommendations/module/<module_id>', methods=['DELETE'])
428def remove_recommendations_of_module(module_id, internal_call=False):
429    if not internal_call:
430        if request.method != 'DELETE': return
431
432    # Check if the user has permissions to access this resource
433    if (not internal_call): views.user.isAuthenticated(request)
434   
435    # 2. Connect to the database and delete the resource
436    try:
437        conn    = mysql.connect()
438        cursor  = conn.cursor()
439        cursor.execute("DELETE FROM recommendation_question_answer WHERE ID IN (SELECT recommendation_question_answer_ID From view_module_recommendations_questions_answers where module_id=%s);", module_id)
440        conn.commit()
441    except Exception as e:
442        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
443    finally:
444        cursor.close()
445        conn.close()
446    
447    # The Delete request was a success, the user 'took the blue pill'.
448    if (not internal_call):
449        return (modules.utils.build_response_json(request.path, 200))
450    else:
451        return(True)
452
453
454"""
455[Summary]: Removes the mapping of a module and a recommendation.
456[Returns]: Returns a reponse object.
457"""
458@app.route('/api/recommendation/<recommendation_id>/module/<module_id>', methods=['DELETE'])
459def remove_recommendation_of_module(recommendation_id, module_id, internal_call=False):
460    if not internal_call:
461        if request.method != 'DELETE': return
462
463    # Check if the user has permissions to access this resource
464    if (not internal_call): views.user.isAuthenticated(request)
465   
466    # 2. Connect to the database and delete the resource
467    try:
468        conn    = mysql.connect()
469        cursor  = conn.cursor()
470        cursor.execute("DELETE FROM recommendation_question_answer WHERE ID IN (SELECT recommendation_question_answer_ID From view_module_recommendations_questions_answers where module_id=%s AND recommendation_id=%s);", (module_id, recommendation_id))
471        conn.commit()
472    except Exception as e:
473        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
474    finally:
475        cursor.close()
476        conn.close()
477    
478    # The Delete request was a success, the user 'took the blue pill'.
479    if (not internal_call):
480        return (modules.utils.build_response_json(request.path, 200))
481    else:
482        return(True)
483

Get Recommendations

GET /api/recommendations
Synopsis

Get recommendations.

Request Headers
Response Headers
Response JSON Object
  • id (int) – Id of the recommendation.

  • content (string) – Recommendation name.

  • description (string) – Description of the recommendation.

  • guide (string) – The filename of the recommendation guide.

  • modules (array) – Array of modules mapped to the current recommendation.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/recommendations":{
      "content":[
         {
            "id":1,
            "content":"Confidentiality",
            "description":null,
            "guide":null,
            "modules":[
               {
                  "id":1,
                  "shortname":"SRE",
                  "displayname":"Security Requirements",
                  "fullname":"Security Requirements Elicitation",
                  "description":"test",
                  "avatar":null,
                  "logic_filename":null,
                  "type_id":null,
                  "createdon":"Sat, 20 Nov 2021 13:29:49 GMT",
                  "updatedon":"Sat, 20 Nov 2021 13:29:49 GMT"
               }
            ],
            "createdOn":"Sat, 20 Nov 2021 13:29:49 GMT",
            "updatedOn":"Sat, 20 Nov 2021 13:29:49 GMT"
         }
      ],
      "status":200
   }
}

GET /api/recommendation/(int: id)
Synopsis

Get recommendation identified by id.

Request Headers
Response Headers
Form Parameters
  • id – The id of the recommendation.

Response JSON Object
  • id (int) – Id of the recommendation.

  • content (string) – Recommendation name.

  • description (string) – Description of the recommendation.

  • guide (string) – The filename of the recommendation guide.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/recommendation/1":{
      "content":[
         {
            "id":1,
            "content":"Confidentiality",
            "description":null,
            "guide":null,
            "createdOn":"Sat, 20 Nov 2021 13:29:49 GMT",
            "updatedOn":"Sat, 20 Nov 2021 13:29:49 GMT"
         }
      ],
      "status":200
   }
}

GET /api/recommendations/module/(int: id)
Synopsis

Finds the set of recommendations for a module identified by id.

Request Headers
Response Headers
Form Parameters
  • id – The id of the module.

Response JSON Object
  • id (int) – Id of the recommendation.

  • content (string) – Recommendation name.

  • questions_answers (array) – An array that contains the mapping between questions and answers for the current recommendation and module.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/recommendations/module/1":{
      "content":[
         {
            "id":1,
            "content":"Confidentiality",
            "questions_answers":[
               {
                  "question_id":1,
                  "answer_id":1,
                  "createdon":"Sat, 20 Nov 2021 13:29:49 GMT",
                  "updatedon":"Sat, 20 Nov 2021 13:29:49 GMT"
               }
            ],
            "createdon":"Sat, 20 Nov 2021 13:29:49 GMT",
            "updatedon":"Sat, 20 Nov 2021 13:29:49 GMT"
         }
      ],
      "status":200
   }
}

Note

In this example, for module 1 the recommendation 1 is given if for question_id the answer_id is given by a user.


Add Recommendation

POST /api/recommendation
Synopsis

Add a new recommendation.

Request Headers
Response Headers
Request JSON Object
  • content (string) – The name of the new recommendation.

  • description (string) – The description of the new recommendation.

  • guide (string) – The filename of the recommendation guide.

  • questions_answers (array) – An array that contains the mapping between questions and answers for the new recommendation.

Response JSON Object
  • id (int) – The id of the new recommendation.

  • status (int) – Status code.

Status Codes

Example Request

{
   "content":"New Recommendation",
   "description":"New Recommendation Description",
   "guide":"new_recommendation.md",
   "questions_answers":[
      {
         "question_id":1,
         "answer_id":1
      }
   ]
}

Important

In order to upload a guide for the new recommendation you need to use this service in conjunction with the service detailed in /api/file/(string:filename).

Example Response

{"/api/recommendation":{"id":4, "status":200}}

Edit Recommendation

PUT /api/recommendation
Synopsis

Update a recommendation identified by id.

Request Headers
Response Headers
Request JSON Object
  • id (string) – The id of the recommendation to update.

  • content (string) – The name of the new recommendation.

  • description (string) – The description of the new recommendation.

  • guide (string) – The filename of the recommendation guide.

Response JSON Object
  • status (int) – Status code.

Status Codes

Example Request

{
   "id":4,
   "content":"New Recommendation updated",
   "description":"New recommendation description updated",
   "guide":"recommendation_updated_guide.md"
}

Important

In order to upload a new guide you need to use this service in conjunction with the service detailed in /api/file/(string:filename).

Example Response

{"/api/recommendation":{"status":200}}

Remove Recommendation

DELETE /api/recommendations/(int: id)
Synopsis

Remove a recommendation identified by id.

Request Headers
Response Headers
Form Parameters
  • id – The id of the recommendation to remove.

Response JSON Object
  • status (int) – Status code.

Status Codes

Example Response

{"/api/recommendation":{"status":200}}

DELETE /api/recommendations/module/(int: id)
Synopsis

Removes the mapping between a module identified by id and its recommendations.

Request Headers
Response Headers
Form Parameters
  • id – The id of the module.

Response JSON Object
  • status (int) – Status code.

Status Codes

Example Response

{"/api/recommendations/modules/1":{"status":200}}

DELETE /api/recommendations/(int: id)/module/(int: id)
Synopsis

Removes the mapping between a module id and a recommendation id.

Request Headers
Response Headers
Form Parameters
  • id – The id of the recommendation or module.

Response JSON Object
  • status (int) – Status code.

Status Codes

Example Response

{"/api/recommendations/1/modules/1":{"status":200}}

Sessions Services API

This section includes details concerning services developed for the session entity implemented in
session.py.
  1"""
  2// ---------------------------------------------------------------------------
  3//
  4//	Security Advising Modules (SAM) for Cloud IoT and Mobile Ecosystem
  5//
  6//  Copyright (C) 2020 Instituto de Telecomunicações (www.it.pt)
  7//  Copyright (C) 2020 Universidade da Beira Interior (www.ubi.pt)
  8//
  9//  This program is free software: you can redistribute it and/or modify
 10//  it under the terms of the GNU General Public License as published by
 11//  the Free Software Foundation, either version 3 of the License, or
 12//  (at your option) any later version.
 13//
 14//  This program is distributed in the hope that it will be useful,
 15//  but WITHOUT ANY WARRANTY; without even the implied warranty of
 16//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17//  GNU General Public License for more details.
 18//
 19//  You should have received a copy of the GNU General Public License
 20//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 21// 
 22//  This work was performed under the scope of Project SECURIoTESIGN with funding 
 23//  from FCT/COMPETE/FEDER (Projects with reference numbers UID/EEA/50008/2013 and 
 24//  POCI-01-0145-FEDER-030657) 
 25// ---------------------------------------------------------------------------
 26"""
 27import shutil
 28from api import app, mysql
 29from flask import request
 30import json, os
 31import modules.error_handlers, modules.utils # SAM's modules
 32import views.user, views.module, views.dependency # SAM's views
 33
 34"""
 35[Summary]: Adds a new session to a user.
 36[Returns]: Response result.
 37"""
 38@app.route('/api/session', methods=['POST'])
 39def add_session():
 40    DEBUG = False
 41    if request.method != 'POST': return
 42    data = {}
 43
 44    # 1. Check if the user has permissions to access this resource.
 45    views.user.isAuthenticated(request)
 46
 47    # 2. Let's get our shiny new JSON object
 48    #    Always start by validating the structure of the json, if not valid send an invalid response.
 49    try:
 50        obj = request.json
 51    except Exception as e:
 52        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
 53    
 54    # 3. Let's validate the data of our JSON object with a custom function.
 55    if (not modules.utils.valid_json(obj, {"email", "module_id"})):
 56        raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)
 57
 58    # 4. Before starting a new session, let's just check if there are any dependencies. That is if the user needs to answer questions from any other module before the current one.
 59    module_dependencies = views.module.find_module(obj['module_id'], True)[0]['dependencies']
 60    
 61    if (DEBUG): modules.utils.console_log("['POST']/api/session", "List of dependencies for module " + str(obj['module_id']) + "=" + str(module_dependencies))
 62    if (len(module_dependencies) != 0):
 63        # 4.1. Let's iterate the list of dependencies and check if the user answered the questions to that module (i.e., a closed session exists)
 64        for module_dependency in module_dependencies:
 65            dep_module_id = module_dependency['module']['id']
 66            user_id = views.user.find_user(obj['email'], True)['id']
 67
 68            # 4.2. Let's get the sesions of the current user for the dependency
 69            user_sessions = count_sessions_of_user_module(dep_module_id, user_id)
 70            if (user_sessions == 0):
 71                data = {}
 72                data['message']      = "A session was not created, the module dependencies are not fulfilled, the module is dependent on one or more modules."
 73                data['dependencies'] = module_dependencies
 74                return (modules.utils.build_response_json(request.path, 404, data))
 75
 76
 77    # 5. Check if there is an identical session closed, 
 78    #    if true, notify the user on the return response object that a new session will be created; otherwise, 
 79    #    delete all that of the previously opened session.
 80    destroySessionID = -1
 81    try:
 82        conn    = mysql.connect()
 83        cursor  = conn.cursor()
 84        cursor.execute("SELECT ID, ended FROM session WHERE userID=(SELECT ID FROM user WHERE email=%s) AND moduleID=%s ORDER BY ID DESC LIMIT 1", (obj['email'], obj['module_id']))
 85        res = cursor.fetchall()
 86    except Exception as e:
 87        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
 88
 89    if (len(res) != 0):
 90        for row in res:
 91            if (row[1] == 1):
 92                data['message'] = "A session for this module was previously created and closed, a new one will be created."
 93            else:
 94                destroySessionID = int(row[0])
 95                data['message'] = "A session for this module was previously created, but it is still open. The session will be deleted and recreated."
 96    cursor.close()
 97
 98    if (destroySessionID != -1):
 99        try:
100            conn    = mysql.connect()
101            cursor  = conn.cursor()
102            cursor.execute("DELETE FROM session WHERE ID=%s", destroySessionID)
103            conn.commit()
104        except Exception as e:
105            raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
106        finally:
107            cursor.close()
108
109    
110    # 6. Connect to the database and add a new session.
111    try:
112        cursor  = conn.cursor()
113        cursor.execute("INSERT INTO session (userID, moduleID) VALUES ((SELECT ID FROM user WHERE email=%s), %s)", (obj['email'],obj['module_id']))
114        data['id'] = conn.insert_id()
115        conn.commit()
116
117    except Exception as e:
118        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
119    finally:
120        cursor.close()
121        conn.close()
122
123    data['module'] = views.module.find_module(obj['module_id'], True)
124
125    # 7. The request was a success, the user 'took the blue pill'.
126    return (modules.utils.build_response_json(request.path, 200, data))
127
128"""
129[Summary]: Updates the session with the set of answers given and the corresponding questions.
130[Returns]: Response result.
131"""
132@app.route('/api/session/<ID>', methods=['PUT'])
133def update_session(ID):
134    if request.method != 'PUT': return
135    # 1. Check if the user has permissions to access this resource
136    views.user.isAuthenticated(request)
137
138    # 2. Let's get our shiny new JSON object.
139    # - Always start by validating the structure of the json, if not valid send an invalid response.
140    try:
141        obj = request.json
142    except Exception as e:
143        raise modules.error_handlers.BadRequest(request.path, str(e), 400) 
144    
145    # print(obj)
146    answer_user_inputted = False
147    # 3. Let's validate the data of our JSON object with a custom function.
148    if (not modules.utils.valid_json(obj, {"question_id","answer_id"})):
149        if (modules.utils.valid_json(obj, {"question_id","input"})): 
150            answer_user_inputted = True
151        else:
152            raise modules.error_handlers.BadRequest(request.path, "Some required key or value is missing from the JSON object", 400)
153
154    try:
155        conn    = mysql.connect()
156        cursor  = conn.cursor()
157        if (not answer_user_inputted):
158            cursor.execute("INSERT INTO session_user_answer (sessionID, questionAnswerID) VALUES (%s, (SELECT ID FROM question_answer WHERE questionID=%s AND answerID=%s))", (ID, obj['question_id'], obj['answer_id']))
159        else:
160            cursor.execute("INSERT INTO session_user_answer (sessionID, questionID, input) VALUES (%s, %s, %s)", (ID, obj['question_id'], obj['input']))
161        conn.commit()
162    except Exception as e:
163        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
164    finally:
165        cursor.close()
166        conn.close()
167
168    # 5. The Update request was a success, the user 'is in the rabbit hole'
169    return (modules.utils.build_response_json(request.path, 200))
170
171
172"""
173[Summary]: Get sessions (opened and closed)
174[Returns]: Returns response result.
175"""
176@app.route('/api/sessions', methods=['GET'])
177def get_sessions():
178    if request.method != 'GET': return
179
180    # 1. Check if the user has permissions to access this resource
181    views.user.isAuthenticated(request)
182
183    # 2. Let's existing sessions from the database.
184    try:
185        conn    = mysql.connect()
186        cursor  = conn.cursor()
187        cursor.execute("SELECT ID, userID, moduleID, ended, createdOn, updatedOn FROM session")
188        res = cursor.fetchall()
189    except Exception as e:
190        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
191    
192    # 2.1. Check for empty results.
193    if (len(res) == 0):
194        cursor.close()
195        conn.close()
196        return(modules.utils.build_response_json(request.path, 404))    
197    else:
198        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
199        for row in res:
200            data = {}
201            data['id']          = row[0]
202            data['user_id']     = row[1]
203            data['module_id']   = row[2]
204            data['ended']       = row[3]
205            data['createdOn']   = row[4]
206            data['updatedOn']   = row[5]
207            datas.append(data)
208        cursor.close()
209        conn.close()
210        # 3. 'May the Force be with you'.
211        return(modules.utils.build_response_json(request.path, 200, datas))    
212
213"""
214[Summary]: Ends a user's session.
215[Returns]: Response result.
216"""
217@app.route('/api/session/<ID>/end', methods=['PUT'])
218def end_session(ID):
219    if request.method != 'PUT': return
220
221    # 1. Check if the user has permissions to access this resource.
222    views.user.isAuthenticated(request)
223
224    # 2. End a session by defining the flag ended to one.
225    try:
226        conn    = mysql.connect()
227        cursor  = conn.cursor()
228        cursor.execute("UPDATE session SET ended=1 WHERE ID=%s", ID) 
229        conn.commit()
230    except Exception as e:
231        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
232    finally:
233        cursor.close()
234        conn.close()
235        # 3. The request was a success, let's generate the recommendations.
236        return find_recommendations(request, ID)
237
238"""
239[Summary]: Gets a session that was previsouly closed. A session is considered closed when all answers were given by the end user.
240[Returns]: Response result.
241"""
242@app.route('/api/session/<ID>/closed', methods=['GET'])
243def find_session_closed(ID, internal_call=False):
244    if (not internal_call):
245        if request.method != 'GET': return
246    
247    # 0. Check if the user has permissions to access this resource.
248    if (not internal_call): views.user.isAuthenticated(request)
249
250    # 1. Let's get the data of the session that has ended.
251    print("getting data of the session that has ended.") 
252    try:
253        conn    = mysql.connect()
254        cursor  = conn.cursor()
255        cursor.execute("SELECT session_ID, session_userID, session_moduleID, session_ended, session_createdOn, session_updatedOn, question_ID, question, answer_input, module_logic, answer_id, answer FROM view_session_answers WHERE session_ID=%s AND session_ended=1 ORDER BY question_ID", ID)
256        res = cursor.fetchall()
257    except Exception as e:
258        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
259    
260    # 2. Check for empty results.
261    print("Checking emoty results")
262    if (len(res) == 0):
263        conn    = mysql.connect()
264        cursor  = conn.cursor()
265        cursor.execute("SELECT ID, userID, moduleID, ended, createdOn, updatedOn FROM session WHERE ID = %s" , ID)
266        res = cursor.fetchall()
267        data = {}
268        # 3. Let's get the info about the session.
269        for row in res:
270            data['id']           = row[0]
271            data['user_id']       = row[1]
272            data['module_id']     = row[2]
273            # data['module_logic'] = row[9] 
274            data['ended']        = row[3]
275            data['createdOn']    = row[4]
276            data['updatedOn']    = row[5]
277            break
278
279        try:
280            cursor  = conn.cursor()
281            cursor.execute("SELECT recommendation_id, recommendation, recommendation_description, recommendation_guide  FROM view_session_recommendation WHERE session_id=%s", ID)
282            res = cursor.fetchall()
283        except Exception as e:
284            raise modules.error_handlers.BadRequest(request.path, str(e), 500)
285
286        if (len(res) != 0):
287            recommendations = []
288            for row in res:
289                recommendation = {}
290                recommendation['id']                  = row[0]
291                recommendation['content']             = row[1]
292                recommendation['description']         = row[2]
293                recommendation['recommendation_guide'] = row[3]
294                recommendations.append(recommendation)
295            data.update({"recommendations": recommendations})
296        conn.close()
297        cursor.close()
298
299        if (internal_call):
300            return(data)
301        else:
302            return(modules.utils.build_response_json(request.path, 400))
303    else:
304        data = {}
305        previous_question_id = 0 # To support multiple choice questions
306        # 3. Let's get the info about the session.
307        for row in res:
308            data['id']           = row[0]
309            data['user_id']       = row[1]
310            data['module_id']     = row[2]
311            # data['module_logic'] = row[9] 
312            data['ended']        = row[3]
313            data['createdOn']    = row[4]
314            data['updatedOn']    = row[5]
315            break
316        # 4. Let's get the questions and inputted answers.
317        questions = []
318        for row in res:
319            question = {}
320            if previous_question_id == 0 or previous_question_id != row[6]:
321                previous_question_id = row[6]
322            else:
323                continue
324            question['id']           = row[6]
325            question['content']      = row[7]
326
327            answers = [] # Empty array of answers
328            for row in res:
329                if row[6] != question['id']:
330                    continue
331                # Let's check if the answer was user inputted, or selected from the database.
332                if (row[8] != None):
333                    answers.append({ "ID": -1, "content": row[8]})
334                else:
335                    answers.append({ "ID": row[10], "content": row[11]})
336            
337            question['answer'] = answers
338            questions.append(question)
339        data.update({"questions":  questions})
340    cursor.close()
341    
342    # 5. Let's now get the recommendations, if any, stored for this session
343    print("Getting recomendations stored")
344    try:
345        cursor  = conn.cursor()
346        cursor.execute("SELECT recommendation_id, recommendation, recommendation_description, recommendation_guide  FROM view_session_recommendation WHERE session_id=%s", ID)
347        res = cursor.fetchall()
348    except Exception as e:
349        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
350
351    if (len(res) != 0):
352        recommendations = []
353        for row in res:
354            recommendation = {}
355            recommendation['id']                  = row[0]
356            recommendation['content']             = row[1]
357            recommendation['description']         = row[2]
358            recommendation['recommendation_guide'] = row[3]
359            recommendations.append(recommendation)
360        data.update({"recommendations": recommendations})
361    conn.close()
362    cursor.close()
363
364    if (internal_call): 
365        return(data) 
366    else: 
367        return(modules.utils.build_response_json(request.path, 200, data))
368
369
370"""
371[Summary]: Gets closed sessions. A session is considered closed when all answers were given by the end user.
372[Returns]: Response result.
373"""
374@app.route('/api/sessions/closed', methods=['GET'])
375def get_sessions_closed(internal_call=False):
376    if (not internal_call):
377        if request.method != 'GET': return
378    
379    # Check if the user has permissions to access this resource.
380    if (not internal_call): views.user.isAuthenticated(request)
381
382    # Let's get the list of sessions from the db.
383    try:
384        conn    = mysql.connect()
385        cursor  = conn.cursor()
386        cursor.execute("SELECT id FROM session WHERE ended =1")
387        res = cursor.fetchall()
388    except Exception as e:
389        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
390    
391    # Check for empty results.
392    if (len(res) == 0):
393        cursor.close()
394        conn.close()
395        if (internal_call):
396            return(None)
397        else:
398            return(modules.utils.build_response_json(request.path, 400))
399
400    sessions = []
401    for row in res:
402        session = find_session_closed(row[0], True)
403        if (session): sessions.append(session)
404    cursor.close()
405    conn.close()
406
407    # 'May the Force be with you'.
408    if (internal_call): 
409        return(sessions) 
410    else: 
411        return(modules.utils.build_response_json(request.path, 200, sessions))
412
413
414"""
415[Summary]: Finds a session by ID (opened or closed)
416[Returns]: Returns response result.
417"""
418@app.route('/api/session/<ID>', methods=['GET'])
419def find_session(ID, internal_call=False):
420    if (not internal_call):
421        if request.method != 'GET': return
422
423    # 1. Check if the user has permissions to access this resource
424    if (not internal_call): views.user.isAuthenticated(request)
425
426    # 2. Let's existing sessions from the database.
427    try:
428        conn    = mysql.connect()
429        cursor  = conn.cursor()
430        cursor.execute("SELECT ID, userID, moduleID, ended, createdOn, updatedOn FROM session WHERE ID=%s", ID)
431        res = cursor.fetchall()
432    except Exception as e:
433        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
434    
435    # 2.1. Check for empty results.
436    if (len(res) == 0):
437        cursor.close()
438        conn.close()
439        if (not internal_call):
440            return(modules.utils.build_response_json(request.path, 404))
441        else:
442            return(None)
443    else:
444        # 2.2. Check if the session requested was ended.
445        if (res[0][3] == 1):
446            cursor.close()
447            conn.close()
448            datas = find_session_closed(ID, True)
449            if (datas == None):
450                if (not internal_call):
451                    return(modules.utils.build_response_json(request.path, 404))
452                else: 
453                    return(None)
454            else:
455                if (not internal_call):
456                    return(modules.utils.build_response_json(request.path, 200, datas)) 
457                else:
458                    return(datas)
459        
460        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
461        for row in res:
462            data = {}
463            data['id']          = row[0]
464            data['user_id']     = row[1]
465            data['module_id']   = row[2]
466            data['ended']       = row[3]
467            data['createdOn']   = row[4]
468            data['updatedOn']   = row[5]
469            datas.append(data)
470        cursor.close()
471        conn.close()
472
473        # 3. 'May the Force be with you'.
474        if (not internal_call):
475            return(modules.utils.build_response_json(request.path, 200, datas))    
476        else:
477            return(datas)
478
479"""
480[Summary]: Finds sessions by user email (opened or closed)
481[Returns]: Returns response result.
482"""
483@app.route('/api/sessions/user/<user_email>', methods=['GET'])
484def find_sessions_of_user(user_email, internal_call=False):
485    if (not internal_call):
486        if request.method != 'GET': return
487
488    # Check if the user has permissions to access this resource
489    if (not internal_call): 
490        views.user.isAuthenticated(request)
491
492    # Let's existing sessions from the database.
493    try:
494        conn    = mysql.connect()
495        cursor  = conn.cursor()
496        cursor.execute("SELECT session_id, user_id, user_email, moduleID, ended, createdon, updatedon FROM view_user_sessions WHERE user_email=%s", user_email)
497        res = cursor.fetchall()
498    except Exception as e:
499        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
500    
501    # Check for empty results.
502    if (len(res) == 0):
503        cursor.close()
504        conn.close()
505        if (not internal_call):
506            return(modules.utils.build_response_json(request.path, 404))
507        else:
508            return(None)
509    else:
510        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
511        for row in res:
512            data = {}
513            data['id']          = row[0]
514            data['user_id']     = row[1]
515            data['user_email']  = row[2]
516            data['ended']       = row[4]
517            data['createdOn']   = row[5]
518            data['updatedOn']   = row[6]
519            session         = find_session_closed(data['id'], True)
520            if session:
521                if ("questions" in session): 
522                    questions               = session['questions']
523                    data['questions']       = questions
524                if ("recommendations" in session):
525                    recommendations         = session['recommendations']
526                    data['recommendations'] = recommendations
527            module = views.module.find_module(row[3], True)
528            del module[0]['recommendations']
529            del module[0]['tree']
530            if (module): data['module'] = module[0]
531            datas.append(data)
532        cursor.close()
533        conn.close()
534        
535        # 'May the Force be with you'.
536        if (not internal_call):
537            return(modules.utils.build_response_json(request.path, 200, datas))
538        else:
539            return(datas)
540
541"""
542[Summary]: Finds sessions by module ID and user email (closed)
543[Returns]: Returns response result.
544"""
545@app.route('/api/sessions/module/<module_id>/user/<user_id>', methods=['GET'])
546def find_sessions_of_user_module(module_id, user_id, internal_call=False):
547    if (not internal_call):
548        if request.method != 'GET': return
549
550    # Check if the user has permissions to access this resource
551    if (not internal_call): 
552        views.user.isAuthenticated(request)
553
554    # Let's existing sessions from the database.
555    try:
556        conn    = mysql.connect()
557        cursor  = conn.cursor()
558        cursor.execute("SELECT session_id, user_id, user_email, moduleID, ended, createdon, updatedon FROM view_user_sessions WHERE moduleID=%s AND user_id=%s AND ended=1 ORDER BY session_id DESC", (module_id, user_id))
559        res = cursor.fetchall()
560    except Exception as e:
561        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
562    
563    # Check for empty results.
564    if (len(res) == 0):
565        cursor.close()
566        conn.close()
567        if (not internal_call):
568            return(modules.utils.build_response_json(request.path, 404))
569        else:
570            return(None)
571    else:
572        datas = [] # Create a new nice empty array of dictionaries to be populated with data from the DB.
573        for row in res:
574            data = {}
575            data['id']          = row[0]
576            data['user_id']     = row[1]
577            data['user_email']  = row[2]
578            data['module_id']   = row[3]
579            data['ended']       = row[4]
580            data['createdOn']   = row[5]
581            data['updatedOn']   = row[6]
582            datas.append(data)
583        cursor.close()
584        conn.close()
585        
586        # 'May the Force be with you'.
587        if (not internal_call):
588            return(modules.utils.build_response_json(request.path, 200, datas))
589        else:
590            return(datas)
591
592"""
593[Summary]: After closing the session we need to find the recommendations based on the answers given on that session <ID>.
594[Returns]: Response result.
595"""
596def find_recommendations(request, ID):
597    # 1. Is there a logic file to process the set of answers given in this session? If yes, then, run the logic file. 
598    #    This element will be in charge of calling the service to return one or more recommendations, depending on the implemented logic.  
599    #    Otherwise, use the static information present in the database to infer the set of recommendations.
600    session = (find_session_closed(ID, True))
601    
602    if (session is None):
603        raise modules.error_handlers.BadRequest(request.path, "Unable to find recommendations for this session, maybe, there is something wrong with the answers given in this session.", 403) 
604
605    module               = views.module.find_module(session['module_id'], True)
606    tree                 = views.module.get_module_tree(str(session['module_id']), True)
607    # Checks if tree exists
608    if (tree != None):
609        ordered_questions    = modules.utils.order_questions(tree, session['questions'], [])
610
611    #if (session['module_logic'] != None):
612    if (module[0]['logic_filename'] != None):
613        try:
614            # 2.1. Get information related to the current session. This includes the information about the module, the set of related questions, and the answers given by the user to those questions.
615            json_session = json.loads(json.dumps(find_session(ID, True), indent=4, sort_keys=False, default=str))
616            # Replace the questions with ordered questions
617            # If questions exist
618            if(tree != None):
619                json_session['questions'] = ordered_questions
620            # 2.2. Get the set of available recommendations.
621            json_recommendations = json.loads(json.dumps(views.recommendation.get_recommendations(True), indent=4, sort_keys=False, default=str))
622            
623            # 2.3 Get dependencies of the current module, including the last sessions there were flagged has being closed.
624            module_id   = json_session['module_id']
625            user_id     = json_session['user_id']
626            dependencies = views.dependency.find_dependency_of_module(module_id, True)
627
628            if (dependencies):
629                for dependency in dependencies:
630                    del dependency['id']
631                    del dependency['createdon']
632                    del dependency['updatedon']
633                    dep_module_id = dependency['module']['id']
634                    # Get the last session of each dependency
635                    last_session_id    = (find_sessions_of_user_module(dep_module_id, user_id, True)[0])['id']
636                    # Get the answers given to that last session 
637                    last_session = find_session(last_session_id, True)
638                    dependency['module']['last_session'] = last_session
639            
640            json_session['dependencies'] =  json.loads(json.dumps(dependencies, indent=4, sort_keys=False, default=str))
641
642            # 2.4. Dynamically load the logic element for the current session.
643            module_logic_filename = module[0]['logic_filename']
644            module_logic_filename = module_logic_filename[0: module_logic_filename.rfind('.')] # Remove file extension
645            # name = "external." + module_logic_filename + "." + module_logic_filename
646            mod = __import__('external.' + module_logic_filename, fromlist=[''])
647            try:
648                provided_recommendations = mod.run(json_session, json_recommendations)
649            except Exception as e:
650                modules.utils.console_log("logic_file", str(e))
651                raise modules.error_handlers.BadRequest(request.path, str(e), 500)
652                
653            # 2.5. Make the recommendations taking into account the results of the logic element.
654            if (len(provided_recommendations) != 0):
655                for recommendation_id in provided_recommendations:
656                    add_logic_session_recommendation(ID, recommendation_id)
657
658        except Exception as e:
659            modules.utils.console_log("end_session", str(e))
660            raise modules.error_handlers.BadRequest(request.path, str(e), 500)
661
662    # 2. Get the list of recommendations to be stored in the session.
663    #    -> Some redundancy is expected to exist on the database, however, it avoids further 
664    #       processing when checking the history of sessions.
665    try:
666        conn    = mysql.connect()
667        cursor  = conn.cursor()
668        if (module[0]['logic_filename'] != None):
669            table_name = "view_recommendation_logic"
670        else:
671            table_name = "view_recommendation"
672        
673        cursor.execute("SELECT session_id, recommendation_id, recommendation, recommendation_description, guideFileName FROM " + table_name + " WHERE session_id=%s", ID)
674        res = cursor.fetchall()
675    except Exception as e:
676        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
677        
678    # 2.1 Check for empty results.
679    if (len(res) == 0):
680        cursor.close()
681        conn.close()
682        return(modules.utils.build_response_json(request.path, 404))
683    
684    result = {}
685    recommendations = []
686    result['recommendations'] = recommendations
687    for row in res:
688        if ("id" not in result): result['id']   = row[0]
689        recommendation = {}
690        recommendation['id']                    = row[1]
691        recommendation['content']               = row[2]
692        recommendation['description']           = row[3]
693        recommendation['recommendation_guide']  = row[4]
694        recommendations.append(recommendation)
695        # 3. Store the recommendations for the current session, only for those module that are not using any kind of external logic. 
696        if (module[0]['logic_filename'] == None):
697            try:
698                conn2    = mysql.connect()
699                cursor2  = conn2.cursor()
700                cursor2.execute("INSERT INTO session_recommendation (sessionID, recommendationID) VALUES (%s, %s)", (ID, recommendation['id']))
701                conn2.commit()
702            except Exception as e:
703                raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
704            finally:
705                cursor2.close()
706                conn2.close()
707    cursor.close()
708    conn.close()
709    # print(result)
710
711    # 4. Create ZIP with recommendations and guides
712    # 4.1 Create the temporary directory to be zipped
713    temp_dir = 'temp/session'+str(ID)+'/'
714    if not os.path.exists(temp_dir):
715        os.mkdir(temp_dir)
716    # 4.2 Generate the PDF recommendations file    
717    modules.utils.build_recommendations_PDF(module_name=module[0]['shortname'], session_id=ID, recommendations=result)
718    # 4.3 Add guides to the temporary directory
719    for recm in recommendations:
720        if recm['recommendation_guide'] is not None:
721            shutil.copyfile('external/'+str(recm['recommendation_guide']), temp_dir+str(recm['recommendation_guide']))
722    # 4.4 Zip all the files
723    zipped = modules.utils.create_recommendations_ZIP(module_name=module[0]['shortname'], session_id=ID)
724    # 4.5 Remove all the files created to zip
725    if zipped:
726        shutil.rmtree(temp_dir)
727    return(modules.utils.build_response_json(request.path, 200, result))  
728
729
730"""
731[Summary]: Adds a recommendation to a session, this method is exclusively used after the logic of a module is executed.
732[Returns]: 
733"""
734def add_logic_session_recommendation(session_id, recommendation_id):
735    try:
736        conn    = mysql.connect()
737        cursor  = conn.cursor()
738        cursor.execute("INSERT INTO session_recommendation (sessionID, recommendationID) VALUES (%s, %s)", (session_id, recommendation_id))
739        conn.commit()
740    except Exception as e:
741        raise modules.error_handlers.BadRequest(request.path, str(e), 500) 
742    finally:
743        cursor.close()
744        conn.close()
745
746    # The recommendation is linked to the session.
747    return (True)
748
749"""
750[Summary]: Counts number of closed sessions by module ID and user ID.
751[Returns]: Returns response result.
752"""
753def count_sessions_of_user_module(module_id, user_id):
754    # Let's get existing sessions from the database.
755    try:
756        conn    = mysql.connect()
757        cursor  = conn.cursor()
758        cursor.execute("SELECT COUNT(session_id) FROM view_user_sessions WHERE moduleID=%s AND user_id=%s AND ended=1 ORDER BY session_id DESC", (module_id, user_id))
759        res = cursor.fetchall()
760        cursor.close()
761        conn.close()
762    except Exception as e:
763        raise modules.error_handlers.BadRequest(request.path, str(e), 500)
764
765    if (res):
766        return res[0][0]
767    else:
768        return 0

Get User Sessions

GET /api/sessions
Synopsis

Get user sessions.

Request Headers
Response Headers
Response JSON Object
  • id (int) – Id of the session.

  • module_id (int) – Id of the module executed on that session.

  • user_id (int) – The session belongs to this user.

  • ended (boolean) – Flag that indicates that a session was terminated by the user.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/sessions":{
      "content":[
         {
            "id":1,
            "module_id":1,
            "user_id":2,
            "ended":0,
            "updatedOn":"Sat, 20 Nov 2021 10:31:41 GMT",
            "createdOn":"Sat, 20 Nov 2021 10:31:41 GMT"
         }
      ],
      "status":200
   }
}

Note

The end flag should be set to true when all the questions of a module were answered by a user.


GET /api/session/(int: id)
Synopsis

Get user session identified by id.

Request Headers
Response Headers
Parameters
  • id (int) – id of the session.

Response JSON Object
  • id (int) – Id of the session.

  • module_id (int) – Id of the module executed on that session.

  • user_id (int) – The session belongs to this user.

  • ended (boolean) – Flag that indicates that a session was terminated by the user.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/sessions":{
      "content":[
         {
            "id":1,
            "module_id":1,
            "user_id":2,
            "ended":0,
            "updatedOn":"Sat, 20 Nov 2021 10:31:41 GMT",
            "createdOn":"Sat, 20 Nov 2021 10:31:41 GMT"
         }
      ],
      "status":200
   }
}

GET /api/session/user/(string: email)
Synopsis

Get sessions of a user identified by email.

Request Headers
Response Headers
Parameters
  • email (string) – Email of the user.

Response JSON Object
  • id (int) – Id of the session or module.

  • user_email (string) – The email of the user.

  • ended (boolean) – Flag that indicates that a session was terminated by the user.

  • type_id (int) – Module type id.

  • shortname (string) – Unique short name or abbreviation.

  • fullname (string) – Full name of the module.

  • displayname (string) – Module display name that can be used by a frontend.

  • dependencies (array) – An array that contains the set of modules that the current module depends on.

  • description (string) – Module description.

  • avatar (string) – Avatar of the user (i.e., location in disk).

  • logic_filename (string) – Filename of the file containing the dynamic logic of the module.

  • plugin (boolean) – A flag that sets if the current module is a plugin.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/sessions/user/forrest@sam.pt":{
      "content":[
         {

            "id":1,
            "user_email":"forrest@sam.pt",
            "user_id":2,
            "ended":0,
            "module":{
               "id":1,
               "shortname":"SRE",
               "fullname":"Security Requirements Elicitation",
               "displayname":"Security Requirements",
               "description":null,
               "dependencies":[],
               "logic_filename":null,
               "type_id":null,
               "avatar":null,
               "createdon":"Fri, 19 Nov 2021 15:29:18 GMT",
               "updatedon":"Fri, 19 Nov 2021 15:29:18 GMT"
            },
            "createdOn":"Sat, 20 Nov 2021 10:31:41 GMT",
            "updatedOn":"Sat, 20 Nov 2021 10:31:41 GMT"
         }
      ],
      "status":200
   }
}

GET /api/session/closed
Synopsis

Get user sessions that were set as closed or terminated.

Request Headers
Response Headers
Response JSON Object
  • id (int) – Id of the session, question, answer, or recommendation.

  • module_id (int) – Id of the module executed on that session.

  • user_id (int) – The session belongs to this user.

  • ended (boolean) – Flag that indicates that a session was terminated by the user.

  • content (string) – The content of question, answer, or recommendation.

  • answer (array) – Selected answers for the current question and session.

  • recommendation_guide (string) – The filename of the recommendation guide.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/sessions/closed":{
      "content":[
         {
            "id":1,
            "user_id":2,
            "module_id":1,
            "ended":1,
            "questions":[
               {
                  "id":1,
                  "content":"What is the domain of your IoT system ?",
                  "answer":[{"id":1,"content":"Smart home"}],
               }
            ],
            "recommendations":[
               {
                  "id":1,
                  "content":"Confidentiality",
                  "description":null,
                  "recommendation_guide":null
               }
            ],
            "createdOn":"Sat, 20 Nov 2021 10:31:41 GMT",
            "updatedOn":"Sat, 20 Nov 2021 11:31:29 GMT"
         }
      ],
      "status":200
   }
}

GET /api/session/(int: id)/closed
Synopsis

Get user sessions identified by id that were flagged as closed.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the session.

Response JSON Object
  • id (int) – Id of the session, question, answer, or recommendation.

  • module_id (int) – Id of the module executed on that session.

  • user_id (int) – The session belongs to this user.

  • ended (boolean) – Flag that indicates that a session was terminated by the user.

  • content (string) – The content of question, answer, or recommendation.

  • answer (array) – Selected answers for the current question and session.

  • recommendation_guide (string) – The filename of the recommendation guide.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/sessions/closed":{
      "content":[
         {
            "id":1,
            "user_id":2,
            "module_id":1,
            "ended":1,
            "questions":[
               {
                  "id":1,
                  "content":"What is the domain of your IoT system ?",
                  "answer":[{"id":1,"content":"Smart home"}],
               }
            ],
            "recommendations":[
               {
                  "id":1,
                  "content":"Confidentiality",
                  "description":null,
                  "recommendation_guide":null
               }
            ],
            "createdOn":"Sat, 20 Nov 2021 10:31:41 GMT",
            "updatedOn":"Sat, 20 Nov 2021 11:31:29 GMT"
         }
      ],
      "status":200
   }
}

GET /api/session/module/(int: id)/user/(int: id)
Synopsis

Get user sessions that were flagged as closed for a particular user id and module id.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the module or user.

Response JSON Object
  • id (int) – Id of the session.

  • user_id (int) – The session belongs to this user.

  • user_email (string) – The email of the user.

  • module_id (int) – Id of the module executed on that session.

  • ended (boolean) – Flag that indicates that a session was terminated by the user.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Response

{
   "/api/sessions/module/1/user/2":{
      "content":[
         {
            "id":1,
            "user_id":2
            "user_email":"forrest@sam.pt",
            "module_id":1,
            "ended":1,
            "createdOn":"Sat, 20 Nov 2021 10:31:41 GMT",
            "updatedOn":"Sat, 20 Nov 2021 11:31:29 GMT"
         }
      ],
      "status":200
   }
}

Create User Session

POST /api/session
Synopsis

Starts a new user session taking into account a user selected module identified by module_id.

Request Headers
Response Headers
Request JSON Object
  • module_id (int) – Id of the module that will be executed on that user session.

  • email (string) – Email of the user who started the session.

Response JSON Object
  • id (int) – Id of the new session.

  • shortname (string) – Unique short name or abbreviation of the module.

  • fullname (string) – Full name of the module.

  • displayname (string) – Module display name that can be used by a frontend.

  • description (string) – Module description.

  • tree (array) – Array of questions and answers mapped to the module.

  • dependencies (array) – An array that contains the set of modules that the current module depends on.

  • recommendations (array) – Array of recommendations that contains the mapping between question id and answer id.

  • tree – Array of questions and answers mapped to the module.

  • createdon (datetime) – Creation date and time.

  • updatedon (datetime) – Update date and time.

  • status (int) – Status code.

Status Codes

Example Request

{"module_id":1,"email":"forrest@sam.pt"}

Example Response

{
   "/api/session":{
      "id":1,
      "module":[
         {
            "id":1,
            "shortname":"SRE",
            "displayname":"Security Requirements",
            "fullname":"Security Requirements Elicitation",
            "description":"Module description",
            "avatar":null,
            "logic_filename":null,
            "type_id":null,
            "dependencies":[],
            "recommendations":[
               {
                  "id":1,
                  "content":"Confidentiality",
                  "questions_answers":[
                     {
                        "id":1,
                        "question_id":1,
                        "answer_id":1,
                        "createdon":"Sat, 20 Nov 2021 11:55:12 GMT",
                        "updatedon":"Sat, 20 Nov 2021 11:55:12 GMT"
                     }
                  ],
                  "createdon":"Sat, 20 Nov 2021 11:55:12 GMT",
                  "updatedon":"Sat, 20 Nov 2021 11:55:12 GMT"
               }
            ],
            "tree":[
               {
                  "id":1,
                  "order":0,
                  "name":"What is the domain of your IoT system ?",
                  "multipleAnswers":0,
                  "type":"question",
                  "expanded":false,
                  "children":[
                     {"id":1, "name":"Smart home", "type":"answer", "children":[]},
                     {"id":2, "name":"Smart Healthcare", "type":"answer", "children":[]}
                  ]
               }
            ],
            "createdon":"Sat, 20 Nov 2021 11:55:12 GMT",
            "updatedon":"Sat, 20 Nov 2021 11:55:12 GMT"
         }
      ],
      "status":200
   }
}

Edit User Session

PUT /api/session
Synopsis

Update a session. Specifically, by adding an answer to a question for the session module.

Request Headers
Response Headers
Request JSON Object
  • question_id (int) – Id of the module that will be executed on that user session.

  • answer_id (int) – Email of the user who started the session.

  • input (string) – (optional) User direct answer that was not choosen from a predefined set of answers.

Response JSON Object
  • status (int) – Status code.

Status Codes

Example Request

{"question_id":1, "answer_id":1}

Important

The input parameter should only be included in the request body if the question is not a multi-choice one where the user has to include a direct answer:

{"question_id":1, "input":"This is the user answer"}

Example Response

{"/api/session/1":{"status":200}}

Close User Session

PUT /api/session/{id}/end
Synopsis

Close or terminate a user session through the session id.

Request Headers
Response Headers
Parameters
  • id (int) – Id of the session.

Response JSON Object
  • id (int) – Id of the session that was closed.

  • recommendations (array) – An array that contains the recommendations based on the answers given by the user in the session.

  • status (int) – Status code.

Status Codes

Note

This service will return the set of recommendations based on the answers given by the user in that session.

Example Response

{
   "/api/session/1/end":{
      "id":1,
      "recommendations":[
         {
            "id":1,
            "content":"Confidentiality",
            "description":null,
            "recommendation_guide":null
         }
      ],
      "status":200
   }
}

Acknowledgment

This work was performed under the scope of Project SECURIoTESIGN with funding from FCT/COMPETE/FEDER with reference number POCI-01-0145-FEDER-030657. This work is funded by Portuguese FCT/MCTES through national funds and, when applicable, co-funded by EU funds under the project UIDB/50008/2020, research grants BIL/Nº11/2019-B00701, BIL/Nº12/2019-B00702, and FCT research and doctoral grant SFRH/BD/133838/2017 and BIM/n°32/2018-B00582, respectively, and also supported by project CENTRO-01-0145-FEDER-000019 - C4 - Competence Center in Cloud Computing, Research Line 1: Cloud Systems, Work package WP 1.2 - Systems design and development processes and software for Cloud and Internet of Things ecosystems, cofinanced by the European Regional Development Fund (ERDF) through the Programa Operacional Regional do Centro (Centro 2020), in the scope of the Sistema de Apoio à Investigação Científica e Tecnológica - Programas Integrados de IC&DT.

License

Apache License

Version 2.0, January 2004

http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

  1. Definitions.

    “License” shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.

    “Licensor” shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.

    “Legal Entity” shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.

    “You” (or “Your”) shall mean an individual or Legal Entity exercising permissions granted by this License.

    “Source” form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.

    “Object” form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.

    “Work” shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).

    “Derivative Works” shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.

    “Contribution” shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as “Not a Contribution.”

    “Contributor” shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.

  2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.

  3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.

  4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:

    1. You must give any other recipients of the Work or Derivative Works a copy of this License; and

    2. You must cause any modified files to carry prominent notices stating that You changed the files; and

    3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and

    4. If the Work includes a “NOTICE” text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.

    You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.

  5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.

  6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.

  7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.

  8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.

  9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS