All materials
app.py
pyapp.py
import os
import sqlite3
import requests
from flask import Flask, request, render_template_string, jsonify, g, redirect
app = Flask(__name__)
DATABASE = '/app/data/operations.db'
UPLOAD_FOLDER = '/app/uploads'
GUIDE_API_URL = os.environ.get('GUIDE_API_URL', 'http://guide-system:5000/api/guides')
TOURISM_API_URL = os.environ.get('TOURISM_API_URL', 'http://tourism-portal:3000/api/tourist-data')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
def init_db():
db = sqlite3.connect(DATABASE)
db.execute('''
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
uploaded_by TEXT,
category TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
db.execute('''
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender TEXT NOT NULL,
subject TEXT,
body TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
count = db.execute('SELECT COUNT(*) FROM messages').fetchone()[0]
if count == 0:
messages = [
('Tshering Pem', 'Q1 Tourism Statistics', 'Please review the attached Q1 statistics before the board meeting.'),
('Dorji Wangchuk', 'Guide Training Schedule', 'The spring training sessions start next week. All cultural heritage guides must attend.'),
('Admin', 'System Maintenance', 'The operations platform will be down for maintenance on Sunday 6 AM to 8 AM.'),
]
for m in messages:
db.execute('INSERT INTO messages (sender, subject, body) VALUES (?, ?, ?)', m)
db.commit()
db.close()
init_db()
INDEX_TEMPLATE = '''
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Internal Operations Platform</title>
<style>
body { font-family: Helvetica, Arial, sans-serif; margin: 0; padding: 20px; background: #f0f2f5; }
.header { background: #1a3a5c; color: white; padding: 20px; margin: -20px -20px 20px; }
.section { background: white; padding: 15px; margin: 10px 0; border-radius: 6px; border: 1px solid #e0e0e0; }
h2 { color: #1a3a5c; }
form { margin: 10px 0; }
input[type="file"] { margin: 10px 0; }
button { padding: 8px 16px; background: #1a3a5c; color: white; border: none; border-radius: 4px; cursor: pointer; }
table { width: 100%%; border-collapse: collapse; }
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #1a3a5c; color: white; }
</style></head>
<body>
<div class="header"><h1>Internal Operations Platform</h1><p>Bhutan Tourism Council - Staff Communications and Document Management</p></div>
<div class="section">
<h2>Upload Document</h2>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="document">
<select name="category"><option>Report</option><option>Policy</option><option>Communication</option><option>Other</option></select>
<button type="submit">Upload</button>
</form>
</div>
<div class="section">
<h2>Recent Messages</h2>
<table><tr><th>From</th><th>Subject</th><th>Date</th></tr>
{% for m in messages %}<tr><td>{{ m.sender }}</td><td>{{ m.subject }}</td><td>{{ m.created_at }}</td></tr>{% endfor %}
</table>
</div>
</body></html>
'''
@app.route('/')
def index():
db = get_db()
messages = db.execute('SELECT sender, subject, created_at FROM messages ORDER BY created_at DESC LIMIT 10').fetchall()
return render_template_string(INDEX_TEMPLATE, messages=messages)
# VULNERABLE: Upload validation only checks file extension, not content type or magic bytes
ALLOWED_EXTENSIONS = {'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt', '.csv'}
@app.route('/upload', methods=['POST'])
def upload():
if 'document' not in request.files:
return redirect('/')
file = request.files['document']
if file.filename == '':
return redirect('/')
# Only checks extension -- does not verify content type or magic bytes
ext = os.path.splitext(file.filename)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
return 'File type not allowed', 400
filepath = os.path.join(UPLOAD_FOLDER, file.filename)
file.save(filepath)
db = get_db()
category = request.form.get('category', 'Other')
db.execute('INSERT INTO documents (filename, uploaded_by, category) VALUES (?, ?, ?)',
(file.filename, 'staff', category))
db.commit()
return redirect('/')
# Internal API -- no authentication
# Aggregates data from other portals
@app.route('/api/dashboard')
def dashboard():
data = {'guides': [], 'tourists': []}
try:
resp = requests.get(GUIDE_API_URL, timeout=5)
data['guides'] = resp.json().get('guides', [])
except Exception:
pass
try:
resp = requests.get(TOURISM_API_URL, timeout=5)
data['tourists'] = resp.json().get('tourists', [])
except Exception:
pass
return jsonify(data)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)