mirror of https://github.com/docusealco/docuseal
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1112 lines
25 KiB
1112 lines
25 KiB
# Security Architecture - FloDoc
|
|
|
|
**Document**: Security Design & Implementation
|
|
**Version**: 1.0
|
|
**Last Updated**: 2026-01-14
|
|
**Compliance**: POPIA, OWASP Top 10
|
|
|
|
---
|
|
|
|
## 🛡️ Security Overview
|
|
|
|
FloDoc implements defense-in-depth security across all layers. The system handles sensitive student PII and must comply with South African POPIA (Protection of Personal Information Act).
|
|
|
|
**Security Principles**:
|
|
1. **Least Privilege**: Users only access what they need
|
|
2. **Defense in Depth**: Multiple security layers
|
|
3. **Zero Trust**: Verify everything, trust nothing
|
|
4. **Audit Everything**: Complete audit trail
|
|
5. **Secure by Default**: Safe defaults, opt-in for risky features
|
|
|
|
---
|
|
|
|
## 🔐 Authentication
|
|
|
|
### 1. TP Portal (Devise + JWT)
|
|
|
|
**Authentication Flow**:
|
|
```
|
|
User Login (Email/Password)
|
|
↓
|
|
Devise Validates
|
|
↓
|
|
2FA Check (if enabled)
|
|
↓
|
|
JWT Token Generated
|
|
↓
|
|
Token Stored (HTTP-only cookie or localStorage)
|
|
↓
|
|
Subsequent Requests Include Token
|
|
```
|
|
|
|
**Implementation**:
|
|
```ruby
|
|
# app/models/user.rb
|
|
class User < ApplicationRecord
|
|
devise :database_authenticatable, :registerable,
|
|
:recoverable, :rememberable, :validatable,
|
|
:two_factor_authenticatable,
|
|
:otp_secret_encryption_key => ENV['OTP_SECRET_KEY']
|
|
|
|
# JWT token generation
|
|
def generate_jwt
|
|
payload = {
|
|
user_id: id,
|
|
exp: 7.days.from_now.to_i,
|
|
role: role
|
|
}
|
|
JWT.encode(payload, ENV['JWT_SECRET_KEY'], 'HS256')
|
|
end
|
|
|
|
def valid_otp?(code)
|
|
return true unless otp_required_for_login?
|
|
super
|
|
end
|
|
end
|
|
```
|
|
|
|
**Token Storage**:
|
|
```javascript
|
|
// Frontend - Secure storage
|
|
// Option 1: HTTP-only cookie (more secure)
|
|
// Option 2: Memory (less persistent but secure)
|
|
|
|
// Recommended: Memory storage with refresh
|
|
const authStore = defineStore('auth', {
|
|
state: () => ({
|
|
token: null,
|
|
user: null
|
|
}),
|
|
|
|
actions: {
|
|
async login(email, password) {
|
|
const response = await axios.post('/api/v1/auth/token', { email, password })
|
|
this.token = response.data.token
|
|
// Store in memory only
|
|
axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`
|
|
},
|
|
|
|
async refresh() {
|
|
if (this.isTokenExpiringSoon) {
|
|
const response = await axios.post('/api/v1/auth/refresh')
|
|
this.token = response.data.token
|
|
}
|
|
}
|
|
},
|
|
|
|
getters: {
|
|
isTokenExpiringSoon: (state) => {
|
|
if (!state.token) return false
|
|
const decoded = jwtDecode(state.token)
|
|
const expiry = decoded.exp * 1000
|
|
return expiry - Date.now() < 5 * 60 * 1000 // 5 minutes
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
**Session Management**:
|
|
- Token expiry: 7 days
|
|
- Auto-refresh: 5 minutes before expiry
|
|
- Logout: Token invalidation on server
|
|
- Concurrent session limit: 3 sessions per user
|
|
|
|
---
|
|
|
|
### 2. Student Portal (Ad-hoc Token Authentication)
|
|
|
|
**Authentication Flow**:
|
|
```
|
|
TP Creates Enrollment
|
|
↓
|
|
Generate Unique Token (32 bytes)
|
|
↓
|
|
Send Email with Link: /enroll/:token
|
|
↓
|
|
Student Clicks Link
|
|
↓
|
|
Token Validated (1-time use, expires in 7 days)
|
|
↓
|
|
Session Established (no account needed)
|
|
```
|
|
|
|
**Token Generation**:
|
|
```ruby
|
|
# app/models/cohort_enrollment.rb
|
|
class CohortEnrollment < ApplicationRecord
|
|
before_create :generate_secure_token
|
|
|
|
def generate_secure_token
|
|
self.token = SecureRandom.urlsafe_base64(32)
|
|
self.token_expires_at = 7.days.from_now
|
|
end
|
|
|
|
def valid_token?(provided_token)
|
|
return false if token_used?
|
|
return false if token_expires_at < Time.current
|
|
provided_token == token
|
|
end
|
|
|
|
def mark_token_used!
|
|
update!(token_used: true, token_used_at: Time.current)
|
|
end
|
|
end
|
|
```
|
|
|
|
**Token Security**:
|
|
- **Length**: 256-bit (32 bytes URL-safe base64)
|
|
- **Entropy**: High (SecureRandom)
|
|
- **Expiry**: 7 days from creation
|
|
- **One-time use**: Marked as used after first access
|
|
- **Revocation**: Can be regenerated by TP admin
|
|
|
|
**Email Template**:
|
|
```erb
|
|
<!-- app/views/cohort_mailer/enrollment_invitation.html.erb -->
|
|
<p>You've been invited to enroll in <%= @cohort.name %></p>
|
|
|
|
<p>
|
|
<%= link_to 'Click here to enroll',
|
|
student_enroll_url(token: @enrollment.token) %>
|
|
</p>
|
|
|
|
<p>
|
|
This link expires in 7 days and can only be used once.<br>
|
|
If you didn't request this, please ignore this email.
|
|
</p>
|
|
```
|
|
|
|
---
|
|
|
|
### 3. Sponsor Portal (Single Email Token)
|
|
|
|
**Authentication Flow**:
|
|
```
|
|
Cohort Reaches Sponsor Phase
|
|
↓
|
|
Generate Single Token for Bulk Signing
|
|
↓
|
|
Send Email to Sponsor
|
|
↓
|
|
Sponsor Clicks Link: /sponsor/:token
|
|
↓
|
|
Token Validated (expires in 3 days)
|
|
↓
|
|
Bulk Signing Interface
|
|
```
|
|
|
|
**Special Characteristics**:
|
|
- **Single Email Rule**: One email per cohort, not per student
|
|
- **Bulk Operations**: Sign once for all students
|
|
- **Short Expiry**: 3 days (more sensitive)
|
|
- **Read-Only Preview**: Can view before signing
|
|
|
|
**Implementation**:
|
|
```ruby
|
|
# app/models/cohort.rb
|
|
class Cohort < ApplicationRecord
|
|
def generate_sponsor_token
|
|
self.sponsor_token = SecureRandom.urlsafe_base64(32)
|
|
self.sponsor_token_expires_at = 3.days.from_now
|
|
save!
|
|
end
|
|
|
|
def sponsor_token_valid?
|
|
return false if sponsor_token.blank?
|
|
sponsor_token_expires_at > Time.current
|
|
end
|
|
|
|
def send_sponsor_invitation
|
|
generate_sponsor_token
|
|
CohortMailer.sponsor_invitation(self).deliver_later
|
|
end
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
## 🔑 Authorization
|
|
|
|
### 1. Role-Based Access Control (RBAC)
|
|
|
|
**Roles**:
|
|
- `tp_admin`: Full access to TP portal
|
|
- `tp_user`: Limited access (view only)
|
|
- `student`: Ad-hoc access to own enrollment
|
|
- `sponsor`: Ad-hoc access to cohort signing
|
|
|
|
**Ability Class**:
|
|
```ruby
|
|
# app/models/ability.rb
|
|
class Ability
|
|
include CanCanCan
|
|
|
|
def initialize(user)
|
|
return unless user.present?
|
|
|
|
if user.tp_admin?
|
|
can :manage, :all
|
|
cannot :manage, User unless user.super_admin?
|
|
elsif user.tp_user?
|
|
can :read, Cohort, institution_id: user.institution_id
|
|
can :manage, CohortEnrollment, cohort: { institution_id: user.institution_id }
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
**Controller Authorization**:
|
|
```ruby
|
|
# app/controllers/tp/cohorts_controller.rb
|
|
class tp::CohortsController < ApplicationController
|
|
before_action :authenticate_user!
|
|
load_and_authorize_resource
|
|
|
|
def index
|
|
@cohorts = current_institution.cohorts
|
|
end
|
|
|
|
def create
|
|
@cohort = current_institution.cohorts.new(cohort_params)
|
|
if @cohort.save
|
|
redirect_to tp_cohort_path(@cohort)
|
|
else
|
|
render :new
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Ad-hoc Token Authorization
|
|
|
|
**Token-Based CanCanCan**:
|
|
```ruby
|
|
# app/controllers/student/enrollment_controller.rb
|
|
class Student::EnrollmentController < ApplicationController
|
|
skip_before_action :authenticate_user!
|
|
before_action :validate_token
|
|
|
|
def show
|
|
# @enrollment set by before_action
|
|
end
|
|
|
|
private
|
|
|
|
def validate_token
|
|
@enrollment = CohortEnrollment.find_by(token: params[:token])
|
|
|
|
unless @enrollment&.valid_token?(params[:token])
|
|
redirect_to root_path, alert: 'Invalid or expired link'
|
|
return
|
|
end
|
|
|
|
# Auto-mark as used after first access
|
|
@enrollment.mark_token_used! unless @enrollment.token_used?
|
|
end
|
|
end
|
|
```
|
|
|
|
**Authorization Matrix**:
|
|
|
|
| Action | TP Admin | TP User | Student | Sponsor |
|
|
|--------|----------|---------|---------|---------|
|
|
| Create Cohort | ✅ | ❌ | ❌ | ❌ |
|
|
| View Cohorts | ✅ | ✅ | ❌ | ❌ |
|
|
| Manage Enrollments | ✅ | ✅ | ❌ | ❌ |
|
|
| Enroll Self | ❌ | ❌ | ✅ (token) | ❌ |
|
|
| Upload Documents | ❌ | ❌ | ✅ (token) | ❌ |
|
|
| View Cohort (Sponsor) | ❌ | ❌ | ❌ | ✅ (token) |
|
|
| Bulk Sign | ❌ | ❌ | ❌ | ✅ (token) |
|
|
| Finalize Cohort | ✅ | ❌ | ❌ | ❌ |
|
|
|
|
---
|
|
|
|
## 🔒 Data Security
|
|
|
|
### 1. Encryption at Rest
|
|
|
|
**Database Encryption**:
|
|
```ruby
|
|
# app/models/cohort_enrollment.rb
|
|
class CohortEnrollment < ApplicationRecord
|
|
# Encrypt sensitive fields
|
|
encrypts :student_email
|
|
encrypts :student_name
|
|
encrypts :student_surname
|
|
encrypts :student_id
|
|
|
|
# Rails 7+ built-in encryption
|
|
# Requires: ENCRYPTION_KEY in environment
|
|
end
|
|
```
|
|
|
|
**Configuration**:
|
|
```ruby
|
|
# config/initializers/active_record_encryption.rb
|
|
Rails.application.config.active_record.encryption.primary_key = ENV['ENCRYPTION_PRIMARY_KEY']
|
|
Rails.application.config.active_record.encryption.deterministic_key = ENV['ENCRYPTION_DETERMINISTIC_KEY']
|
|
Rails.application.config.active_record.encryption.key_derivation_salt = ENV['ENCRYPTION_SALT']
|
|
```
|
|
|
|
**Key Management**:
|
|
- Use environment variables (never commit keys)
|
|
- Rotate keys periodically
|
|
- Backup keys securely
|
|
- Use AWS KMS or similar for production
|
|
|
|
---
|
|
|
|
### 2. File Upload Security
|
|
|
|
**Active Storage Configuration**:
|
|
```ruby
|
|
# config/storage.yml
|
|
local:
|
|
service: Disk
|
|
root: <%= Rails.root.join("storage") %>
|
|
|
|
minio:
|
|
service: S3
|
|
access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
|
|
secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
|
|
region: us-east-1
|
|
endpoint: <%= ENV['AWS_ENDPOINT_URL'] %>
|
|
bucket: <%= ENV['AWS_BUCKET_NAME'] %>
|
|
```
|
|
|
|
**Upload Validation**:
|
|
```ruby
|
|
# app/models/cohort_enrollment.rb
|
|
class CohortEnrollment < ApplicationRecord
|
|
has_many_attached :documents
|
|
|
|
validate :documents_are_valid
|
|
|
|
private
|
|
|
|
def documents_are_valid
|
|
documents.each do |doc|
|
|
# Size limit (5MB)
|
|
if doc.byte_size > 5.megabytes
|
|
errors.add(:documents, 'must be less than 5MB')
|
|
end
|
|
|
|
# Type validation
|
|
allowed_types = ['application/pdf', 'image/jpeg', 'image/png']
|
|
unless allowed_types.include?(doc.content_type)
|
|
errors.add(:documents, 'must be PDF, JPG, or PNG')
|
|
end
|
|
|
|
# Virus scan (if ClamAV available)
|
|
if defined?(ClamAV)
|
|
clam = ClamAV.instance
|
|
scan = clam.scanfile(doc.path)
|
|
if scan.is_a?(ClamAV::VirusResponse)
|
|
errors.add(:documents, 'contains virus')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
**Content Security Policy**:
|
|
```ruby
|
|
# config/initializers/content_security_policy.rb
|
|
Rails.application.config.content_security_policy do |policy|
|
|
policy.default_src :self
|
|
policy.font_src :self, :https, :data
|
|
policy.img_src :self, :https, :data
|
|
policy.object_src :none
|
|
policy.script_src :self, :https
|
|
policy.style_src :self, :https, :unsafe_inline
|
|
policy.connect_src :self, :https, 'http://localhost:3000' if Rails.env.development?
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
### 3. Input Validation & Sanitization
|
|
|
|
**Model Validations**:
|
|
```ruby
|
|
class Cohort < ApplicationRecord
|
|
validates :name, presence: true, length: { maximum: 100 }
|
|
validates :sponsor_email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
validates :program_type, inclusion: { in: %w[learnership internship candidacy] }
|
|
|
|
# Sanitize inputs
|
|
before_validation :strip_whitespace
|
|
|
|
private
|
|
|
|
def strip_whitespace
|
|
self.name = name&.strip
|
|
self.sponsor_email = sponsor_email&.strip&.downcase
|
|
end
|
|
end
|
|
```
|
|
|
|
**Controller Parameter Sanitization**:
|
|
```ruby
|
|
class Api::V1::CohortsController < Api::V1::BaseController
|
|
def create
|
|
# Strong parameters prevent mass assignment
|
|
@cohort = current_institution.cohorts.new(cohort_params)
|
|
# ...
|
|
end
|
|
|
|
private
|
|
|
|
def cohort_params
|
|
params.require(:cohort).permit(
|
|
:name,
|
|
:program_type,
|
|
:sponsor_email,
|
|
:template_id,
|
|
required_student_uploads: [],
|
|
cohort_metadata: [:start_date, :duration_months, :stipend_amount]
|
|
)
|
|
end
|
|
end
|
|
```
|
|
|
|
**SQL Injection Prevention**:
|
|
```ruby
|
|
# ✅ Safe - Uses ActiveRecord
|
|
Cohort.where("name ILIKE ?", "%#{sanitize_sql_like(search)}%")
|
|
|
|
# ❌ Dangerous - Raw SQL with interpolation
|
|
Cohort.where("name ILIKE '%#{search}%'")
|
|
```
|
|
|
|
**XSS Prevention**:
|
|
```ruby
|
|
# Rails automatically escapes in views
|
|
<%= @cohort.name %> # Safe
|
|
|
|
# Mark as safe only if trusted
|
|
<%= raw @trusted_html %> # Only if you control the content
|
|
```
|
|
|
|
---
|
|
|
|
## 🛡️ Web Security
|
|
|
|
### 1. CSRF Protection
|
|
|
|
**Rails Default**:
|
|
```ruby
|
|
# config/initializers/csrf_protection.rb
|
|
Rails.application.config.action_controller.csrf_protection_enabled = true
|
|
```
|
|
|
|
**API Controllers**:
|
|
```ruby
|
|
class Api::V1::BaseController < ActionController::API
|
|
# APIs typically skip CSRF (use token auth instead)
|
|
skip_before_action :verify_authenticity_token
|
|
end
|
|
```
|
|
|
|
**Web Controllers**:
|
|
```ruby
|
|
class ApplicationController < ActionController::Base
|
|
protect_from_forgery with: :exception
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
### 2. CORS Configuration
|
|
|
|
```ruby
|
|
# config/initializers/cors.rb
|
|
Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
|
allow do
|
|
origins 'http://localhost:3000' if Rails.env.development?
|
|
origins 'https://flodoc.com' if Rails.env.production?
|
|
|
|
resource '/api/v1/*',
|
|
headers: :any,
|
|
methods: [:get, :post, :put, :patch, :delete, :options, :head],
|
|
credentials: false,
|
|
expose: ['Authorization']
|
|
end
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
### 3. Security Headers
|
|
|
|
```ruby
|
|
# config/initializers/secure_headers.rb
|
|
Rails.application.config.middleware.use SecureHeaders::Middleware
|
|
|
|
SecureHeaders::Configuration.configure do |config|
|
|
config.csp = {
|
|
default_src: %w['self'],
|
|
script_src: %w['self' 'unsafe-inline'],
|
|
style_src: %w['self' 'unsafe-inline'],
|
|
img_src: %w['self' data:],
|
|
font_src: %w['self' https: data:],
|
|
connect_src: %w['self'],
|
|
object_src: %w['none'],
|
|
base_uri: %w['self'],
|
|
form_action: %w['self']
|
|
}
|
|
|
|
config.hsts = "max-age=#{1.year.to_i}; includeSubDomains; preload"
|
|
config.x_frame_options = "DENY"
|
|
config.x_content_type_options = "nosniff"
|
|
config.x_xss_protection = "1; mode=block"
|
|
config.referrer_policy = "strict-origin-when-cross-origin"
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 Audit & Logging
|
|
|
|
### 1. Audit Trail
|
|
|
|
**Audit Model**:
|
|
```ruby
|
|
# app/models/audit_log.rb
|
|
class AuditLog < ApplicationRecord
|
|
belongs_to :user, optional: true
|
|
belongs_to :auditable, polymorphic: true
|
|
|
|
enum action: {
|
|
cohort_created: 0,
|
|
cohort_activated: 1,
|
|
enrollment_created: 2,
|
|
enrollment_completed: 3,
|
|
sponsor_signed: 4,
|
|
cohort_finalized: 5,
|
|
document_uploaded: 6,
|
|
token_generated: 7,
|
|
login: 8,
|
|
logout: 9
|
|
}
|
|
|
|
validates :action, :auditable, presence: true
|
|
end
|
|
```
|
|
|
|
**Audit Logger**:
|
|
```ruby
|
|
# app/controllers/concerns/auditable.rb
|
|
module Auditable
|
|
extend ActiveSupport::Concern
|
|
|
|
included do
|
|
after_action :log_action, if: :auditable_action?
|
|
end
|
|
|
|
private
|
|
|
|
def log_action
|
|
AuditLog.create!(
|
|
user: current_user,
|
|
auditable: @auditable,
|
|
action: action_name,
|
|
ip_address: request.remote_ip,
|
|
user_agent: request.user_agent,
|
|
metadata: { params: params.except(:password, :token) }
|
|
)
|
|
end
|
|
|
|
def auditable_action?
|
|
%w[create update destroy].include?(action_name)
|
|
end
|
|
end
|
|
|
|
# Usage in controllers
|
|
class tp::CohortsController < ApplicationController
|
|
include Auditable
|
|
|
|
def create
|
|
@cohort = current_institution.cohorts.new(cohort_params)
|
|
@auditable = @cohort
|
|
# ...
|
|
end
|
|
end
|
|
```
|
|
|
|
**Audit Queries**:
|
|
```ruby
|
|
# Find all actions by a user
|
|
AuditLog.where(user: user).order(created_at: :desc)
|
|
|
|
# Find all actions on a cohort
|
|
AuditLog.where(auditable: cohort)
|
|
|
|
# Find suspicious activity
|
|
AuditLog.where('created_at > ?', 1.hour.ago)
|
|
.where(action: :login)
|
|
.group(:ip_address)
|
|
.count
|
|
.select { |_, count| count > 10 }
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Application Logging
|
|
|
|
```ruby
|
|
# config/environments/production.rb
|
|
config.log_level = :info
|
|
config.logger = ActiveSupport::Logger.new(STDOUT)
|
|
|
|
# Log format
|
|
config.log_tags = [:request_id]
|
|
|
|
# Filter sensitive parameters
|
|
config.filter_parameters += [:password, :token, :secret, :key]
|
|
```
|
|
|
|
**Custom Logging**:
|
|
```ruby
|
|
class CohortWorkflowService
|
|
def initialize(cohort, user)
|
|
@cohort = cohort
|
|
@user = user
|
|
@logger = Rails.logger
|
|
end
|
|
|
|
def advance_to_active
|
|
@logger.info(
|
|
"[CohortWorkflow] User #{@user.id} advancing cohort #{@cohort.id} to active"
|
|
)
|
|
|
|
if @cohort.update(status: 'active')
|
|
@logger.info("[CohortWorkflow] Success: cohort #{@cohort.id}")
|
|
true
|
|
else
|
|
@logger.error(
|
|
"[CohortWorkflow] Failed: cohort #{@cohort.id}, errors: #{@cohort.errors.full_messages}"
|
|
)
|
|
false
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
## 🚨 Incident Response
|
|
|
|
### 1. Security Incident Detection
|
|
|
|
**Suspicious Activity Alerts**:
|
|
```ruby
|
|
# app/services/security_monitor.rb
|
|
class SecurityMonitor
|
|
def self.check_suspicious_activity
|
|
# Multiple failed logins
|
|
failed_logins = AuditLog.where(
|
|
action: :login_failed,
|
|
created_at: 1.hour.ago..Time.current
|
|
).group(:ip_address).count
|
|
|
|
failed_logins.each do |ip, count|
|
|
if count > 5
|
|
SecurityMailer.suspicious_activity(ip, count).deliver_later
|
|
end
|
|
end
|
|
|
|
# Multiple token access from different IPs
|
|
token_usage = AuditLog.where(
|
|
action: :token_access,
|
|
created_at: 24.hours.ago..Time.current
|
|
).group(:token_id, :ip_address).count
|
|
|
|
# Alert if same token used from > 2 IPs
|
|
end
|
|
end
|
|
```
|
|
|
|
**Scheduled Monitoring**:
|
|
```ruby
|
|
# config/schedule.rb (using whenever gem)
|
|
every 30.minutes do
|
|
runner "SecurityMonitor.check_suspicious_activity"
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Incident Response Plan
|
|
|
|
**Severity Levels**:
|
|
|
|
**Critical** (Immediate action):
|
|
- Unauthorized database access
|
|
- Token leak
|
|
- PII exposure
|
|
- Ransomware detected
|
|
|
|
**High** (2 hours):
|
|
- Multiple failed login attempts
|
|
- Suspicious API usage
|
|
- File upload anomalies
|
|
|
|
**Medium** (24 hours):
|
|
- Unusual traffic patterns
|
|
- Failed webhook deliveries
|
|
- Performance degradation
|
|
|
|
**Response Steps**:
|
|
1. **Contain**: Disable affected accounts/tokens
|
|
2. **Investigate**: Review audit logs
|
|
3. **Remediate**: Patch vulnerability
|
|
4. **Notify**: Inform affected parties (POPIA requirement)
|
|
5. **Document**: Create incident report
|
|
6. **Prevent**: Implement additional controls
|
|
|
|
---
|
|
|
|
## 🔒 POPIA Compliance
|
|
|
|
### 1. Data Minimization
|
|
|
|
**Principle**: Only collect what's necessary
|
|
|
|
**Implementation**:
|
|
```ruby
|
|
# app/models/cohort_enrollment.rb
|
|
class CohortEnrollment < ApplicationRecord
|
|
# Only collect required fields
|
|
validates :student_email, presence: true
|
|
validates :student_name, presence: true, if: :required_for_program?
|
|
|
|
def required_for_program?
|
|
cohort.program_type == 'learnership'
|
|
end
|
|
end
|
|
```
|
|
|
|
### 2. Purpose Limitation
|
|
|
|
**Principle**: Use data only for specified purposes
|
|
|
|
**Implementation**:
|
|
```ruby
|
|
# Clear purpose statements in UI
|
|
# app/views/student/enrollment/show.html.erb
|
|
<h2>Document Upload</h2>
|
|
<p>
|
|
Your documents will be used for:
|
|
<ul>
|
|
<li>Verification by training provider</li>
|
|
<li>Submission to sponsor for signing</li>
|
|
<li>Legal compliance with learnership agreement</li>
|
|
</ul>
|
|
</p>
|
|
```
|
|
|
|
### 3. Data Retention
|
|
|
|
**Principle**: Don't keep data longer than necessary
|
|
|
|
**Implementation**:
|
|
```ruby
|
|
# app/models/cohort.rb
|
|
class Cohort < ApplicationRecord
|
|
# Auto-archive after 2 years
|
|
scope :for_archival, -> { where('created_at < ?', 2.years.ago) }
|
|
|
|
def archive!
|
|
# Move to cold storage
|
|
# Anonymize PII
|
|
update!(status: 'archived')
|
|
end
|
|
end
|
|
|
|
# Scheduled job
|
|
class ArchiveOldDataJob < ApplicationJob
|
|
def perform
|
|
Cohort.for_archival.find_each(&:archive!)
|
|
end
|
|
end
|
|
```
|
|
|
|
### 4. Right to Access & Deletion
|
|
|
|
**Implementation**:
|
|
```ruby
|
|
# app/controllers/api/v1/user/data_controller.rb
|
|
class Api::V1::User::DataController < Api::V1::BaseController
|
|
# GET /api/v1/user/data/export
|
|
def export
|
|
data = {
|
|
profile: current_user,
|
|
enrollments: current_user.cohort_enrollments,
|
|
audit_logs: AuditLog.where(user: current_user)
|
|
}
|
|
send_data data.to_json, filename: "my-data-#{Date.current}.json"
|
|
end
|
|
|
|
# DELETE /api/v1/user/data
|
|
def delete
|
|
# Anonymize instead of hard delete for audit
|
|
current_user.anonymize!
|
|
sign_out
|
|
head :no_content
|
|
end
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
## 🛡️ OWASP Top 10 Mitigation
|
|
|
|
### A01:2021 - Broken Access Control
|
|
- ✅ Role-based authorization (CanCanCan)
|
|
- ✅ Institution isolation
|
|
- ✅ Token-based ad-hoc access
|
|
- ✅ Audit all access attempts
|
|
|
|
### A02:2021 - Cryptographic Failures
|
|
- ✅ HTTPS enforced
|
|
- ✅ Password hashing (Devise bcrypt)
|
|
- ✅ JWT signing with secrets
|
|
- ✅ Database encryption
|
|
|
|
### A03:2021 - Injection
|
|
- ✅ ActiveRecord parameterized queries
|
|
- ✅ Strong parameters
|
|
- ✅ Input validation
|
|
- ✅ SQL LIKE sanitization
|
|
|
|
### A04:2021 - Insecure Design
|
|
- ✅ Threat modeling completed
|
|
- ✅ Security by default
|
|
- ✅ Defense in depth
|
|
- ✅ Least privilege principle
|
|
|
|
### A05:2021 - Security Misconfiguration
|
|
- ✅ Secure headers
|
|
- ✅ CORS configuration
|
|
- ✅ No default credentials
|
|
- ✅ Environment-based configs
|
|
|
|
### A06:2021 - Vulnerable and Outdated Components
|
|
- ✅ Regular dependency updates
|
|
- ✅ Security scanning (bundler-audit)
|
|
- ✅ Ruby/Rails version maintained
|
|
- ✅ Vue.js security updates
|
|
|
|
### A07:2021 - Identification and Authentication Failures
|
|
- ✅ 2FA for TP users
|
|
- ✅ Token expiration
|
|
- ✅ Rate limiting
|
|
- ✅ Secure token generation
|
|
|
|
### A08:2021 - Software and Data Integrity Failures
|
|
- ✅ Signed JWT tokens
|
|
- ✅ Webhook signature verification
|
|
- ✅ Code signing in CI/CD
|
|
- ✅ Dependency lock files
|
|
|
|
### A09:2021 - Security Logging and Monitoring Failures
|
|
- ✅ Comprehensive audit logs
|
|
- ✅ Failed login tracking
|
|
- ✅ Suspicious activity alerts
|
|
- ✅ Log retention policy
|
|
|
|
### A10:2021 - Server-Side Request Forgery (SSRF)
|
|
- ✅ Webhook URL validation
|
|
- ✅ No arbitrary URL fetching
|
|
- ✅ Internal network restrictions
|
|
- ✅ URL whitelist for webhooks
|
|
|
|
---
|
|
|
|
## 🔍 Security Testing
|
|
|
|
### 1. Static Analysis
|
|
|
|
```bash
|
|
# Ruby security scanning
|
|
bundle exec brakeman
|
|
bundle exec bundler-audit
|
|
|
|
# JavaScript security scanning
|
|
npm audit
|
|
yarn audit
|
|
|
|
# Code quality
|
|
bundle exec rubocop --only security
|
|
```
|
|
|
|
### 2. Dynamic Testing
|
|
|
|
```ruby
|
|
# spec/security/authentication_spec.rb
|
|
RSpec.describe 'Authentication Security', type: :request do
|
|
it 'rejects invalid JWT tokens' do
|
|
get '/api/v1/cohorts', headers: { 'Authorization' => 'Bearer invalid' }
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
|
|
it 'prevents token reuse after logout' do
|
|
token = user.generate_jwt
|
|
user.invalidate_all_tokens!
|
|
get '/api/v1/cohorts', headers: { 'Authorization' => "Bearer #{token}" }
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
```
|
|
|
|
### 3. Penetration Testing Checklist
|
|
|
|
**Authentication**:
|
|
- [ ] Brute force protection
|
|
- [ ] Session fixation prevention
|
|
- [ ] Token leakage protection
|
|
- [ ] 2FA bypass attempts
|
|
|
|
**Authorization**:
|
|
- [ ] Horizontal privilege escalation
|
|
- [ ] Vertical privilege escalation
|
|
- [ ] IDOR (Insecure Direct Object Reference)
|
|
- [ ] Mass assignment
|
|
|
|
**Input Validation**:
|
|
- [ ] SQL injection
|
|
- [ ] XSS (reflected, stored, DOM)
|
|
- [ ] Command injection
|
|
- [ ] File upload bypass
|
|
|
|
**API Security**:
|
|
- [ ] Rate limiting bypass
|
|
- [ ] CORS misconfiguration
|
|
- [ ] Authentication bypass
|
|
- [ ] Information disclosure
|
|
|
|
---
|
|
|
|
## 📊 Security Metrics
|
|
|
|
**Track These Metrics**:
|
|
- Failed login attempts per day
|
|
- Token reuse attempts
|
|
- Unauthorized access attempts
|
|
- Security incidents per month
|
|
- Time to detect incidents
|
|
- Time to remediate incidents
|
|
- Dependency vulnerabilities
|
|
- Code coverage for security tests
|
|
|
|
**Dashboard**:
|
|
```ruby
|
|
# Admin dashboard security widget
|
|
class SecurityDashboard
|
|
def self.metrics
|
|
{
|
|
failed_logins: AuditLog.where(action: :login_failed, created_at: 24.hours.ago..).count,
|
|
suspicious_ips: AuditLog.where('created_at > ?', 1.hour.ago).group(:ip_address).count.select { |_, c| c > 5 },
|
|
tokens_issued: CohortEnrollment.where('created_at > ?', 24.hours.ago).count,
|
|
security_incidents: SecurityIncident.where('created_at > ?', 30.days.ago).count
|
|
}
|
|
end
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 Security Checklist
|
|
|
|
### Application Security
|
|
- [ ] All inputs validated
|
|
- [ ] SQL injection prevented
|
|
- [ ] XSS prevented
|
|
- [ ] CSRF protected
|
|
- [ ] Authentication required
|
|
- [ ] Authorization enforced
|
|
- [ ] Sensitive data encrypted
|
|
- [ ] Secure headers configured
|
|
- [ ] CORS properly configured
|
|
- [ ] Rate limiting implemented
|
|
|
|
### Data Security
|
|
- [ ] Database encryption enabled
|
|
- [ ] File uploads validated
|
|
- [ ] PII minimized
|
|
- [ ] Retention policy defined
|
|
- [ ] Backup encryption
|
|
- [ ] Access logging enabled
|
|
|
|
### Authentication & Authorization
|
|
- [ ] 2FA for TP users
|
|
- [ ] Token expiration
|
|
- [ ] Secure token generation
|
|
- [ ] Session management
|
|
- [ ] Role-based access control
|
|
- [ ] Institution isolation
|
|
|
|
### Monitoring & Logging
|
|
- [ ] Audit logs implemented
|
|
- [ ] Failed login tracking
|
|
- [ ] Suspicious activity alerts
|
|
- [ ] Incident response plan
|
|
- [ ] Log retention policy
|
|
- [ ] Security metrics tracked
|
|
|
|
### Compliance
|
|
- [ ] POPIA compliance
|
|
- [ ] OWASP Top 10 addressed
|
|
- [ ] Security testing performed
|
|
- [ ] Documentation complete
|
|
- [ ] Incident response ready
|
|
|
|
---
|
|
|
|
## 🔐 Environment Security
|
|
|
|
### Development
|
|
```bash
|
|
# .env.example (never commit .env)
|
|
DATABASE_URL=postgresql://localhost/flo_doc
|
|
SECRET_KEY_BASE=dev_secret
|
|
JWT_SECRET_KEY=dev_jwt_secret
|
|
ENCRYPTION_PRIMARY_KEY=dev_encryption_key
|
|
```
|
|
|
|
### Production
|
|
```bash
|
|
# Use environment variables or secrets manager
|
|
DATABASE_URL=postgresql://...
|
|
SECRET_KEY_BASE=$(rails secret)
|
|
JWT_SECRET_KEY=$(openssl rand -base64 32)
|
|
ENCRYPTION_PRIMARY_KEY=$(openssl rand -base64 32)
|
|
```
|
|
|
|
**Never commit secrets**:
|
|
```gitignore
|
|
# .gitignore
|
|
.env
|
|
.env.production
|
|
config/secrets.yml
|
|
config/master.key
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Security Resources
|
|
|
|
- **OWASP Top 10**: https://owasp.org/Top10/
|
|
- **POPIA Guide**: https://www.justice.gov.za/legislation/acts/2013-013.pdf
|
|
- **Rails Security**: https://guides.rubyonrails.org/security.html
|
|
- **Vue.js Security**: https://vuejs.org/guide/best-practices/security.html
|
|
|
|
---
|
|
|
|
**Document Status**: ✅ Complete
|
|
**Last Security Review**: 2026-01-14
|
|
**Next Review**: After Phase 1 Implementation |