Welcome to SAM API’s Documentation

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.
Clone the public repository.
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
.
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).
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 instatistic.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Statistics successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve statistics.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Response JSON Object
size (int) – The total number of registered users.
- Status Codes
200 OK – Statistics successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve statistics.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Statistics successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve statistics.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Response JSON Object
size (int) – The total number of questions.
status (int) – Status code.
- Status Codes
200 OK – Statistics successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve statistics.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Response JSON Object
size (int) – The total number of answers.
status (int) – Status code.
- Status Codes
200 OK – Statistics successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve statistics.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Statistics successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve statistics.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Response JSON Object
top (array) – An array that contains the number of sessions per day.
status (int) – Status code.
- Status Codes
200 OK – Statistics successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve statistics.
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 inuser.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
Content-Type – multipart/form-data
- 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
200 OK – Users successfully authenticated.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
401 Unauthorized – Fail to authenticate user / Unauthorized.
500 Internal Server Error – Internal server error.
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
Authorization – Bearer token provided by /api/user/login.
- Response JSON Object
status (int) – status code.
- Status Codes
200 OK – User was successfully logged out.
500 Internal Server Error – Unable to logout user.
Example Response
{
"/api/user/logout":{
"status":200
}
}
User Services API
This section includes details concerning services developed for the group entity implemented inuser.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Users successfully retrieved.
500 Internal Server Error – Fail to retrieve users.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – User successfully retrieved.
500 Internal Server Error – Fail to retrieve user.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – User groups successfully retrieved.
500 Internal Server Error – Fail to retrieve user groups.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Check performed to see if the user is an administrator.
500 Internal Server Error – Fail to verify user group.
Example Response
{
"/api/user/admin@sam.pt/admin":{
"admin":1,
"status":200
}
}
Add User
- POST /api/user
- Synopsis
Add a new user.
- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – New user added.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to add user.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – User updated.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to update user.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
email – The email of the user to be removed.
- Response JSON Object
status (int) – status code.
- Status Codes
200 OK – User successfully removed.
500 Internal Server Error – Fail to remove user.
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 ingroup.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Groups successfully retrieved.
500 Internal Server Error – Fail to retrieve groups.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Groups successfully retrieved.
500 Internal Server Error – Fail to retrieve groups.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – New group added with id (see example response).
500 Internal Server Error – Fail to add a new group.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Group successfully updated.
500 Internal Server Error – Fail to update group.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
id – The id of the group to be removed.
- Response JSON Object
status (int) – status code.
- Status Codes
200 OK – Group successfully removed.
500 Internal Server Error – Fail to remove group.
Example Response
{"/api/group/3":{"status":200}}
File Services API
This section includes details concerning services developed for the file entity implemented infile.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – multipart/form-data
- Parameters
filename – The name of the file.
- Form Parameters
file – The file to be uploaded.
- Response JSON Object
status (int) – status code.
- Status Codes
201 Created – The file was successfully uploaded.
400 Bad Request – Fail to upload file.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
filename – The name of the file.
- Status Codes
201 Created – The file was successfully uploaded.
400 Bad Request – Fail to upload file.
Get Files
- GET /api/file/(string: filename)
- Synopsis
Returns the list of files available on the predefined
UPLOAD_DIRECTORY
.- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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 byshortname
.- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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 intype.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Types successfully retrieved.
500 Internal Server Error – Fail to retrieve types.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Type successfully retrieved.
500 Internal Server Error – Fail to retrieve type.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Request JSON Object
name (string) – Name of the type.
description (string) – Description of the type.
- Response JSON Object
status (int) – status code.
- Status Codes
200 OK – Type successfully added.
500 Internal Server Error – Fail to add type.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Type successfully updated.
500 Internal Server Error – Fail to update type.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
id – The id of the type to be removed.
- Response JSON Object
status (int) – status code.
- Status Codes
200 OK – Type successfully removed.
500 Internal Server Error – Fail to remove type.
Example Response
{"/api/type/1":{"status":200}}
Module Services API
This section includes details concerning services developed for the module entity implemented inmodule.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Module successfully retrieved.
500 Internal Server Error – Fail to retrieve module.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Module successfully retrieved.
500 Internal Server Error – Fail to retrieve module.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Module type successfully retrieved.
500 Internal Server Error – Fail to retrieve module type information.
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 byid
.- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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 ananswer
.multipleAnswers (boolean) – Defines if the user can select multiple answers for the current question.
status (int) – status code.
- Status Codes
200 OK – Module tree successfully retrieved.
500 Internal Server Error – Fail to retrieve tree of the module.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Response JSON Object
id (int) – Module or question id.
questions (array) – Array of questions.
status (int) – status code.
- Status Codes
200 OK – Module’s questions successfully retrieved.
500 Internal Server Error – Fail to retrieve module’s questions.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Module’s questions successfully retrieved.
500 Internal Server Error – Fail to retrieve module’s questions.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Response JSON Object
id (int) – Module or answer id.
answers (array) – Array of answers.
status (int) – status code.
- Status Codes
200 OK – Module’s answers successfully retrieved.
500 Internal Server Error – Fail to retrieve module’s answers.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Module’s answers successfully retrieved.
500 Internal Server Error – Fail to retrieve module’s answers.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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 answerid
.dependencies (array) – An array that contains the set of modules that the current module depends on.
- Status Codes
200 OK – Module successfully added.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to add module.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Module successfully updated.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to update module.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
id (int) – Id of the module to partially delete.
- Status Codes
200 OK – Module successfully removed.
500 Internal Server Error – Fail to remove module.
- DELETE /api/module/(int: id)/full
- Synopsis
Fully removes a module (including sessions, linked questions, and answers).
- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
id (int) – Id of the module to fully delete.
- Status Codes
200 OK – Module successfully removed.
500 Internal Server Error – Fail to remove module.
- DELETE /api/module/(int: id)/questions
- Synopsis
Removes all questions mapped to the module identified by
id
.- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
id (int) – Id of the module.
- Status Codes
200 OK – Module questions successfully removed.
500 Internal Server Error – Fail to remove module questions.
- DELETE /api/module/(int: id)/answers
- Synopsis
Removes all answers mapped to the module identified by
id
.- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
id (int) – Id of the module.
- Status Codes
200 OK – Module answers successfully removed.
500 Internal Server Error – Fail to remove module answers.
- DELETE /api/module/(int: id)/logic
- Synopsis
Removes the logic file mapped to the module identified by
id
.- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
id (int) – Id of the module.
- Status Codes
200 OK – Module answers successfully removed.
500 Internal Server Error – Fail to remove module answers.
Dependencies Services API
This section includes details concerning services developed for the dependency entity implemented independency.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Dependencies successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve dependencies.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Dependency successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve dependency.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Dependencies successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve dependencies.
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 moduleid
.- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Dependency successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve dependency.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Dependency successfully added.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to add dependency.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Dependency successfully updated.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to updated dependency.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Form Parameters
id – The id of dependency to remove.
- Response JSON Object
status (int) – Status code.
- Status Codes
200 OK – Dependency successfully removed.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to remove dependency.
Example Response
{"/api/dependency":{"status":200}}
Questions Services API
This section includes details concerning services developed for the question entity implemented inquestion.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Questions successfully retrieved.
500 Internal Server Error – Fail to retrieve questions.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Question successfully retrieved.
500 Internal Server Error – Fail to retrieve question.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Questions answers successfully retrieved.
500 Internal Server Error – Fail to retrieve questions answers.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Question answers successfully retrieved.
500 Internal Server Error – Fail to retrieve question answers.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Question successfully added.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to add question.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Question successfully updated.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to update question.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
id (int) – Id of the question to remove.
- Response JSON Object
status (string) – Status code.
- Status Codes
200 OK – Question successfully removed.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to remove question.
Example Response
{"/api/question":{"status":200}}
Answers Services API
This section includes details concerning services developed for the answer entity implemented inanswer.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Answers successfully retrieved.
500 Internal Server Error – Fail to retrieve answers.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Answers successfully retrieved.
500 Internal Server Error – Fail to retrieve answers.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Answer successfully added.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to add answer.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Answer successfully updated.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to update answer.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Parameters
id (int) – Id of the answer to remove.
- Response JSON Object
status (string) – Status code.
- Status Codes
200 OK – Answer successfully removed.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to answer question.
Example Response
{"/api/answer":{"status":200}}
Recommendations Services API
This section includes details concerning services developed for the recommendation entity implemented inrecommendation.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Recommendations successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve recommendations.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Recommendation successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve recommendation.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Recommendations successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve recommendations.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Recommendation successfully added.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to add recommendation.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Recommendation successfully updated.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to update recommendation.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Form Parameters
id – The id of the recommendation to remove.
- Response JSON Object
status (int) – Status code.
- Status Codes
200 OK – Recommendation successfully removed.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to remove recommendation.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Form Parameters
id – The id of the module.
- Response JSON Object
status (int) – Status code.
- Status Codes
200 OK – Mapping successfully removed.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to remove mapping.
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 recommendationid
.- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- Form Parameters
id – The id of the recommendation or module.
- Response JSON Object
status (int) – Status code.
- Status Codes
200 OK – Mapping successfully removed.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to remove mapping.
Example Response
{"/api/recommendations/1/modules/1":{"status":200}}
Sessions Services API
This section includes details concerning services developed for the session entity implemented insession.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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Sessions successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve sessions.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Sessions successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve sessions.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Sessions successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve sessions.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Sessions successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve sessions.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Sessions successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve sessions.
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 moduleid
.- Request Headers
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Sessions successfully retrieved.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to retrieve sessions.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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 answerid
.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
200 OK – Session successfully created.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to create session.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Session successfully created.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to create session.
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
Authorization – Bearer token provided by /api/user/login.
- Response Headers
Content-Type – application/json
- 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
200 OK – Session successfully closed.
400 Bad Request – The server was unable to process the request (e.g., malformed request syntax).
500 Internal Server Error – Fail to close session.
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
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.
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.
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.
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:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
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
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.
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.
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.
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.
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.
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