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.
541 lines
14 KiB
541 lines
14 KiB
# FloDoc Security Test Plan (Phase 4)
|
|
|
|
## Overview
|
|
This document outlines the comprehensive security testing requirements for the FloDoc Institution Management system, following Winston's IV4 integration verification.
|
|
|
|
## Test Categories
|
|
|
|
### 1. Model Layer Security Tests
|
|
|
|
#### 1.1 Data Isolation Tests
|
|
```ruby
|
|
# spec/models/institution_spec.rb
|
|
describe 'Data Isolation' do
|
|
it 'Institution.for_user returns only accessible institutions' do
|
|
user1 = create(:user)
|
|
user2 = create(:user)
|
|
inst1 = create(:institution, account: user1.account)
|
|
inst2 = create(:institution, account: user2.account)
|
|
|
|
expect(Institution.for_user(user1)).to include(inst1)
|
|
expect(Institution.for_user(user1)).not_to include(inst2)
|
|
end
|
|
|
|
it 'User.can_access_institution? works correctly' do
|
|
user = create(:user)
|
|
inst = create(:institution, account: user.account)
|
|
|
|
# Create access
|
|
create(:account_access, user: user, institution: inst)
|
|
|
|
expect(user.can_access_institution?(inst)).to be true
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 1.2 Token Security Tests
|
|
```ruby
|
|
# spec/models/cohort_admin_invitation_spec.rb
|
|
describe 'Token Security' do
|
|
it 'generates 512-bit secure tokens' do
|
|
invitation = build(:cohort_admin_invitation)
|
|
token = invitation.generate_token
|
|
|
|
expect(token.length).to be >= 64
|
|
expect(invitation.hashed_token).not_to eq(token)
|
|
expect(invitation.token_preview).to match(/\A.{8}\.\.\.\z/)
|
|
end
|
|
|
|
it 'validates token with Redis single-use enforcement' do
|
|
invitation = create(:cohort_admin_invitation)
|
|
token = invitation.generate_token
|
|
invitation.save!
|
|
|
|
# First validation should succeed
|
|
expect(invitation.valid_token?(token)).to be true
|
|
|
|
# Second validation should fail
|
|
expect(invitation.valid_token?(token)).to be false
|
|
end
|
|
|
|
it 'rejects expired tokens' do
|
|
invitation = create(:cohort_admin_invitation, expires_at: 1.hour.ago)
|
|
token = invitation.generate_token
|
|
|
|
expect(invitation.valid_token?(token)).to be false
|
|
end
|
|
|
|
it 'rejects wrong email' do
|
|
invitation = create(:cohort_admin_invitation, email: 'correct@example.com')
|
|
token = invitation.generate_token
|
|
|
|
# Simulate wrong email by creating new user with different email
|
|
wrong_user = create(:user, email: 'wrong@example.com')
|
|
|
|
expect(invitation.valid_token?(token)).to be true
|
|
# But acceptance should fail
|
|
result = InvitationService.accept_invitation(token, wrong_user)
|
|
expect(result).to be_nil
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 1.3 Rate Limiting Tests
|
|
```ruby
|
|
# spec/services/invitation_service_spec.rb
|
|
describe 'Rate Limiting' do
|
|
it 'prevents more than 5 invitations per email' do
|
|
institution = create(:institution)
|
|
user = create(:user)
|
|
email = 'test@example.com'
|
|
|
|
# Create 5 valid invitations
|
|
5.times do
|
|
create(:cohort_admin_invitation,
|
|
institution: institution,
|
|
email: email,
|
|
used_at: nil,
|
|
expires_at: 24.hours.from_now)
|
|
end
|
|
|
|
expect {
|
|
InvitationService.create_invitation(institution, email, 'cohort_admin', user)
|
|
}.to raise_error(RateLimit::LimitApproached)
|
|
end
|
|
|
|
it 'allows new invitations after expiration' do
|
|
institution = create(:institution)
|
|
user = create(:user)
|
|
email = 'test@example.com'
|
|
|
|
# Create 5 expired invitations
|
|
5.times do
|
|
create(:cohort_admin_invitation,
|
|
institution: institution,
|
|
email: email,
|
|
used_at: nil,
|
|
expires_at: 1.hour.ago)
|
|
end
|
|
|
|
expect {
|
|
InvitationService.create_invitation(institution, email, 'cohort_admin', user)
|
|
}.not_to raise_error
|
|
end
|
|
end
|
|
```
|
|
|
|
### 2. Request/Controller Security Tests
|
|
|
|
#### 2.1 Cross-Institution Access Tests
|
|
```ruby
|
|
# spec/requests/api/v1/institutions_spec.rb
|
|
describe 'Cross-Institution Access Prevention' do
|
|
let(:user1) { create(:user) }
|
|
let(:user2) { create(:user) }
|
|
let(:inst1) { create(:institution, account: user1.account) }
|
|
let(:inst2) { create(:institution, account: user2.account) }
|
|
|
|
it 'prevents access to other institutions' do
|
|
sign_in user1
|
|
|
|
# Try to access user2's institution
|
|
get "/api/v1/institutions/#{inst2.id}"
|
|
|
|
expect(response).to have_http_status(:forbidden)
|
|
expect(SecurityEvent.where(event_type: 'unauthorized_institution_access').count).to eq(1)
|
|
end
|
|
|
|
it 'prevents updates to other institutions' do
|
|
sign_in user1
|
|
|
|
patch "/api/v1/institutions/#{inst2.id}", params: { institution: { name: 'Hacked' } }
|
|
|
|
expect(response).to have_http_status(:forbidden)
|
|
expect(inst2.reload.name).not_to eq('Hacked')
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 2.2 Role-Based Access Tests
|
|
```ruby
|
|
# spec/requests/api/v1/admin/invitations_spec.rb
|
|
describe 'Role-Based Authorization' do
|
|
let(:institution) { create(:institution) }
|
|
let(:super_admin) { create(:user) }
|
|
let(:admin) { create(:user) }
|
|
|
|
before do
|
|
create(:account_access, user: super_admin, institution: institution, role: 'cohort_super_admin')
|
|
create(:account_access, user: admin, institution: institution, role: 'cohort_admin')
|
|
end
|
|
|
|
it 'allows super admin to create invitations' do
|
|
sign_in super_admin
|
|
|
|
post '/api/v1/admin/invitations', params: {
|
|
institution_id: institution.id,
|
|
email: 'newadmin@example.com',
|
|
role: 'cohort_admin'
|
|
}
|
|
|
|
expect(response).to have_http_status(:created)
|
|
end
|
|
|
|
it 'prevents regular admin from creating invitations' do
|
|
sign_in admin
|
|
|
|
post '/api/v1/admin/invitations', params: {
|
|
institution_id: institution.id,
|
|
email: 'newadmin@example.com',
|
|
role: 'cohort_admin'
|
|
}
|
|
|
|
expect(response).to have_http_status(:forbidden)
|
|
end
|
|
end
|
|
```
|
|
|
|
### 3. Token Security Scenarios
|
|
|
|
#### 3.1 Concurrent Token Validation
|
|
```ruby
|
|
# spec/requests/api/v1/admin/invitation_acceptance_spec.rb
|
|
describe 'Concurrent Token Validation' do
|
|
it 'handles race conditions correctly' do
|
|
invitation = create(:cohort_admin_invitation)
|
|
token = invitation.generate_token
|
|
invitation.save!
|
|
|
|
user = create(:user, email: invitation.email)
|
|
|
|
# Simulate 50 concurrent requests
|
|
results = 50.times.map do
|
|
Thread.new do
|
|
InvitationService.accept_invitation(token, user)
|
|
end
|
|
end
|
|
|
|
# Only one should succeed
|
|
successful = results.map(&:value).compact
|
|
expect(successful.length).to eq(1)
|
|
|
|
# Verify token is marked as used
|
|
expect(invitation.reload.used_at).not_to be_nil
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 3.2 Token Reuse Prevention
|
|
```ruby
|
|
it 'prevents token reuse across different users' do
|
|
invitation = create(:cohort_admin_invitation)
|
|
token = invitation.generate_token
|
|
invitation.save!
|
|
|
|
user1 = create(:user, email: invitation.email)
|
|
user2 = create(:user, email: invitation.email)
|
|
|
|
# First user accepts
|
|
result1 = InvitationService.accept_invitation(token, user1)
|
|
expect(result1).not_to be_nil
|
|
|
|
# Second user tries to use same token
|
|
result2 = InvitationService.accept_invitation(token, user2)
|
|
expect(result2).to be_nil
|
|
end
|
|
```
|
|
|
|
### 4. Security Event Logging Tests
|
|
|
|
#### 4.1 Event Capture Tests
|
|
```ruby
|
|
# spec/models/security_event_spec.rb
|
|
describe 'Security Event Logging' do
|
|
it 'logs all 6 event types' do
|
|
user = create(:user)
|
|
|
|
events = [
|
|
:unauthorized_institution_access,
|
|
:insufficient_privileges,
|
|
:token_validation_failure,
|
|
:rate_limit_exceeded,
|
|
:invitation_accepted,
|
|
:super_admin_demoted
|
|
]
|
|
|
|
events.each do |event_type|
|
|
SecurityEvent.log(event_type, user, { test: true })
|
|
end
|
|
|
|
expect(SecurityEvent.count).to eq(6)
|
|
end
|
|
|
|
it 'captures IP address and details' do
|
|
event = SecurityEvent.log(:unauthorized_institution_access, nil, {
|
|
ip_address: '192.168.1.100',
|
|
institution_id: 123,
|
|
attempted_action: 'show'
|
|
})
|
|
|
|
expect(event.ip_address).to eq('192.168.1.100')
|
|
expect(event.details['institution_id']).to eq(123)
|
|
expect(event.details['attempted_action']).to eq('show')
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 4.2 Alert Threshold Tests
|
|
```ruby
|
|
describe 'Alert Thresholds' do
|
|
it 'detects unauthorized access threshold' do
|
|
user = create(:user)
|
|
|
|
# Create 5 unauthorized access attempts
|
|
5.times do
|
|
SecurityEvent.log(:unauthorized_institution_access, user, {
|
|
institution_id: 999
|
|
})
|
|
end
|
|
|
|
expect(SecurityEvent.alert_threshold_exceeded?(
|
|
'unauthorized_institution_access',
|
|
threshold: 5,
|
|
time_window: 1.hour
|
|
)).to be true
|
|
end
|
|
|
|
it 'detects token failure threshold' do
|
|
user = create(:user)
|
|
|
|
# Create 20 token failures
|
|
20.times do
|
|
SecurityEvent.log(:token_validation_failure, user, {
|
|
reason: 'invalid_token'
|
|
})
|
|
end
|
|
|
|
expect(SecurityEvent.alert_threshold_exceeded?(
|
|
'token_validation_failure',
|
|
threshold: 20,
|
|
time_window: 1.hour
|
|
)).to be true
|
|
end
|
|
end
|
|
```
|
|
|
|
### 5. Performance Tests
|
|
|
|
#### 5.1 Query Performance
|
|
```ruby
|
|
# spec/performance/institution_query_spec.rb
|
|
describe 'Institution Query Performance' do
|
|
it 'performs scoped queries efficiently' do
|
|
# Setup: 1000 institutions, 100 users
|
|
account = create(:account)
|
|
1000.times { create(:institution, account: account) }
|
|
users = create_list(:user, 100, account: account)
|
|
|
|
# Benchmark
|
|
expect {
|
|
Institution.for_user(users.first).limit(10).to_a
|
|
}.to perform_under(50).ms
|
|
end
|
|
|
|
it 'handles concurrent user loads' do
|
|
# 100 concurrent users accessing different institutions
|
|
users = create_list(:user, 100)
|
|
institutions = create_list(:institution, 100)
|
|
|
|
threads = users.zip(institutions).map do |user, institution|
|
|
Thread.new do
|
|
create(:account_access, user: user, institution: institution)
|
|
institution.accessible_by?(user)
|
|
end
|
|
end
|
|
|
|
threads.each(&:join)
|
|
expect(AccountAccess.count).to eq(100)
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 5.2 Redis Performance
|
|
```ruby
|
|
describe 'Redis Token Enforcement' do
|
|
it 'handles high concurrent token validation' do
|
|
invitation = create(:cohort_admin_invitation)
|
|
token = invitation.generate_token
|
|
invitation.save!
|
|
|
|
# 50 concurrent validation attempts
|
|
results = 50.times.map do
|
|
Thread.new do
|
|
invitation.valid_token?(token)
|
|
end
|
|
end
|
|
|
|
# Only first should succeed
|
|
valid_count = results.map(&:value).count(true)
|
|
expect(valid_count).to eq(1)
|
|
end
|
|
end
|
|
```
|
|
|
|
### 6. Integration Tests
|
|
|
|
#### 6.1 Complete Invitation Flow
|
|
```ruby
|
|
# spec/system/invitation_flow_spec.rb
|
|
describe 'Complete Invitation Flow' do
|
|
it 'handles full invitation acceptance workflow' do
|
|
# 1. Super admin creates invitation
|
|
super_admin = create(:user)
|
|
institution = create(:institution)
|
|
create(:account_access, user: super_admin, institution: institution, role: 'cohort_super_admin')
|
|
|
|
sign_in super_admin
|
|
post '/api/v1/admin/invitations', params: {
|
|
institution_id: institution.id,
|
|
email: 'newadmin@example.com',
|
|
role: 'cohort_admin'
|
|
}
|
|
|
|
invitation = CohortAdminInvitation.last
|
|
token = invitation.generate_token # In real flow, this is in Redis
|
|
|
|
# 2. New user accepts invitation
|
|
new_user = create(:user, email: 'newadmin@example.com')
|
|
|
|
result = InvitationService.accept_invitation(token, new_user)
|
|
expect(result).not_to be_nil
|
|
|
|
# 3. Verify access granted
|
|
expect(new_user.cohort_admin?).to be true
|
|
expect(new_user.institutions).to include(institution)
|
|
|
|
# 4. Verify security events logged
|
|
expect(SecurityEvent.where(event_type: 'invitation_created').count).to eq(1)
|
|
expect(SecurityEvent.where(event_type: 'invitation_accepted').count).to eq(1)
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 6.2 Migration Rollback Test
|
|
```ruby
|
|
# spec/migrations/rollback_spec.rb
|
|
describe 'Migration Rollback' do
|
|
it 'rolls back without data loss' do
|
|
# Setup data
|
|
institution = create(:institution)
|
|
user = create(:user)
|
|
invitation = create(:cohort_admin_invitation, institution: institution)
|
|
|
|
# Run rollback
|
|
`bin/rails db:rollback STEP=6`
|
|
|
|
# Verify data integrity
|
|
expect(User.exists?(user.id)).to be true
|
|
expect(Account.exists?(institution.account_id)).to be true
|
|
|
|
# Verify original functionality still works
|
|
expect(Template.count).to eq(0) # No templates created yet
|
|
end
|
|
end
|
|
```
|
|
|
|
### 7. Penetration Testing Scenarios
|
|
|
|
#### 7.1 SQL Injection Attempts
|
|
```ruby
|
|
describe 'SQL Injection Prevention' do
|
|
it 'prevents SQL injection in scoped queries' do
|
|
user = create(:user)
|
|
|
|
# Attempt SQL injection
|
|
malicious_id = "1; DROP TABLE users; --"
|
|
|
|
expect {
|
|
Institution.for_user(user).find_by(id: malicious_id)
|
|
}.not_to raise_error
|
|
|
|
# Verify users table still exists
|
|
expect(User.count).to be >= 1
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 7.2 Token Brute Force
|
|
```ruby
|
|
describe 'Token Brute Force Protection' do
|
|
it 'rate limits token validation attempts' do
|
|
invitation = create(:cohort_admin_invitation)
|
|
wrong_tokens = Array.new(100) { SecureRandom.hex(32) }
|
|
|
|
# Simulate brute force
|
|
results = wrong_tokens.map do |token|
|
|
invitation.valid_token?(token)
|
|
end
|
|
|
|
# All should fail
|
|
expect(results.all?(false)).to be true
|
|
|
|
# Check security events logged
|
|
expect(SecurityEvent.where(event_type: 'token_validation_failure').count).to eq(100)
|
|
end
|
|
end
|
|
```
|
|
|
|
## Test Execution Plan
|
|
|
|
### Prerequisites
|
|
1. Install Ruby 3.4.2
|
|
2. Run `bundle install`
|
|
3. Start Redis: `sudo systemctl start redis-server`
|
|
4. Start PostgreSQL: `sudo systemctl start postgresql`
|
|
5. Run migrations: `bin/rails db:migrate`
|
|
|
|
### Test Execution Commands
|
|
|
|
```bash
|
|
# Run all security tests
|
|
bundle exec rspec spec/models/security_event_spec.rb
|
|
bundle exec rspec spec/models/cohort_admin_invitation_spec.rb
|
|
bundle exec rspec spec/services/invitation_service_spec.rb
|
|
bundle exec rspec spec/requests/api/v1/admin/security_events_spec.rb
|
|
|
|
# Run performance tests
|
|
bundle exec rspec spec/performance/
|
|
|
|
# Run integration tests
|
|
bundle exec rspec spec/system/
|
|
|
|
# Run penetration tests
|
|
bundle exec rspec spec/requests/api/v1/institutions_spec.rb
|
|
bundle exec rspec spec/requests/api/v1/admin/invitation_acceptance_spec.rb
|
|
|
|
# Run all tests with coverage
|
|
bundle exec rspec --format documentation --color
|
|
```
|
|
|
|
### Success Criteria
|
|
|
|
✅ **All tests must pass with:**
|
|
- 80% minimum code coverage
|
|
- Zero security event violations in production
|
|
- Performance within 10% of baseline
|
|
- All 6 security event types logged correctly
|
|
- Token system handles 50+ concurrent requests
|
|
- Rollback procedure verified
|
|
|
|
### Test Results Documentation
|
|
|
|
Create `docs/qa/security-test-results-YYYYMMDD.md` with:
|
|
- Test execution summary
|
|
- Performance metrics
|
|
- Security event counts
|
|
- Penetration test findings
|
|
- Remediation recommendations
|
|
|
|
---
|
|
|
|
**Status:** 📋 **READY FOR EXECUTION** (pending Ruby environment setup) |