s3-secure 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +89 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +56 -0
- data/Rakefile +14 -0
- data/exe/s3-secure +14 -0
- data/lib/s3-secure.rb +1 -0
- data/lib/s3_secure.rb +14 -0
- data/lib/s3_secure/abstract_base.rb +17 -0
- data/lib/s3_secure/autoloader.rb +27 -0
- data/lib/s3_secure/aws_services.rb +36 -0
- data/lib/s3_secure/batch.rb +25 -0
- data/lib/s3_secure/cli.rb +37 -0
- data/lib/s3_secure/command.rb +82 -0
- data/lib/s3_secure/completer.rb +159 -0
- data/lib/s3_secure/completer/script.rb +6 -0
- data/lib/s3_secure/completer/script.sh +10 -0
- data/lib/s3_secure/encryption.rb +27 -0
- data/lib/s3_secure/encryption/base.rb +4 -0
- data/lib/s3_secure/encryption/disable.rb +18 -0
- data/lib/s3_secure/encryption/enable.rb +42 -0
- data/lib/s3_secure/encryption/list.rb +28 -0
- data/lib/s3_secure/encryption/show.rb +18 -0
- data/lib/s3_secure/help.rb +9 -0
- data/lib/s3_secure/help/completion.md +20 -0
- data/lib/s3_secure/help/completion_script.md +3 -0
- data/lib/s3_secure/help/hello.md +5 -0
- data/lib/s3_secure/policy.rb +27 -0
- data/lib/s3_secure/policy/base.rb +4 -0
- data/lib/s3_secure/policy/checker.rb +15 -0
- data/lib/s3_secure/policy/document.rb +27 -0
- data/lib/s3_secure/policy/document/base.rb +15 -0
- data/lib/s3_secure/policy/document/force_ssl_only_access.rb +33 -0
- data/lib/s3_secure/policy/document/force_ssl_only_access_remove.rb +33 -0
- data/lib/s3_secure/policy/enforce.rb +36 -0
- data/lib/s3_secure/policy/list.rb +29 -0
- data/lib/s3_secure/policy/show.rb +19 -0
- data/lib/s3_secure/policy/unforce.rb +41 -0
- data/lib/s3_secure/version.rb +3 -0
- data/s3-secure.gemspec +33 -0
- data/spec/lib/cli_spec.rb +12 -0
- data/spec/lib/policy/checker_spec.rb +68 -0
- data/spec/lib/policy/document/force_ssl_remove_spec.rb +107 -0
- data/spec/lib/policy/document_spec.rb +68 -0
- data/spec/spec_helper.rb +29 -0
- metadata +252 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
class S3Secure::Policy::Document
|
2
|
+
class Base
|
3
|
+
extend Memoist
|
4
|
+
|
5
|
+
def initialize(bucket, bucket_policy)
|
6
|
+
# @bucket_policy is existing document policy
|
7
|
+
@bucket, @bucket_policy = bucket, bucket_policy
|
8
|
+
end
|
9
|
+
|
10
|
+
def checker
|
11
|
+
S3Secure::Policy::Checker.new(@bucket_policy)
|
12
|
+
end
|
13
|
+
memoize :checker
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class S3Secure::Policy::Document
|
2
|
+
class ForceSSLOnlyAccess < Base
|
3
|
+
def policy_document
|
4
|
+
if @bucket_policy.blank?
|
5
|
+
full_policy_document
|
6
|
+
else
|
7
|
+
updated_policy_document
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def updated_policy_document
|
12
|
+
policy = JSON.load(@bucket_policy)
|
13
|
+
policy["Statement"] << ssl_enforce_statement unless checker.has?("ForceSSLOnlyAccess")
|
14
|
+
policy
|
15
|
+
end
|
16
|
+
|
17
|
+
def full_policy_document
|
18
|
+
{"Version"=>"2012-10-17",
|
19
|
+
"Statement"=>[ssl_enforce_statement]}
|
20
|
+
end
|
21
|
+
|
22
|
+
def ssl_enforce_statement
|
23
|
+
{
|
24
|
+
"Sid"=>"ForceSSLOnlyAccess",
|
25
|
+
"Effect"=>"Deny",
|
26
|
+
"Principal"=>"*",
|
27
|
+
"Action"=>"s3:GetObject",
|
28
|
+
"Resource"=>"arn:aws:s3:::#{@bucket}/*",
|
29
|
+
"Condition"=>{"Bool"=>{"aws:SecureTransport"=>"false"}}
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class S3Secure::Policy::Document
|
2
|
+
class ForceSSLOnlyAccessRemove < Base
|
3
|
+
def initialize(bucket, bucket_policy)
|
4
|
+
# @bucket_policy is existing document policy
|
5
|
+
@bucket, @bucket_policy = bucket, bucket_policy
|
6
|
+
end
|
7
|
+
|
8
|
+
def policy_document
|
9
|
+
return nil if @bucket_policy.blank?
|
10
|
+
|
11
|
+
updated_policy_document
|
12
|
+
end
|
13
|
+
|
14
|
+
def updated_policy_document
|
15
|
+
policy = JSON.load(@bucket_policy)
|
16
|
+
|
17
|
+
statements = policy["Statement"]
|
18
|
+
has_force_ssl = !!statements.detect { |s| s["Sid"] == "ForceSSLOnlyAccess" }
|
19
|
+
unless has_force_ssl
|
20
|
+
raise "Bucket policy does not have ForceSSLOnlyAccess"
|
21
|
+
end
|
22
|
+
|
23
|
+
if statements.size == 1
|
24
|
+
return nil # to signal for the entire bucket policy to be deleted
|
25
|
+
else
|
26
|
+
statements.delete_if { |s| s["Sid"] == "ForceSSLOnlyAccess" }
|
27
|
+
policy["Statement"] = statements
|
28
|
+
end
|
29
|
+
|
30
|
+
policy
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class S3Secure::Policy
|
2
|
+
class Enforce < Base
|
3
|
+
def initialize(options={})
|
4
|
+
super
|
5
|
+
@sid = options[:sid]
|
6
|
+
end
|
7
|
+
|
8
|
+
def run
|
9
|
+
@s3 = s3_regional_client(@bucket)
|
10
|
+
|
11
|
+
list = S3Secure::Policy::List.new(@options)
|
12
|
+
list.set_s3(@s3)
|
13
|
+
|
14
|
+
bucket_policy = list.get_policy(@bucket)
|
15
|
+
document = Document.new(@bucket, bucket_policy)
|
16
|
+
if document.has?(@sid)
|
17
|
+
puts "Bucket policy for #{@bucket} has ForceSSLOnlyAccess policy statement already:"
|
18
|
+
puts bucket_policy
|
19
|
+
else
|
20
|
+
# Set encryption rules
|
21
|
+
# Ruby docs: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_bucket_policy-instance_method
|
22
|
+
# API docs: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ServerSideEncryptionByDefault.html
|
23
|
+
#
|
24
|
+
# put_bucket_policy returns #<struct Aws::EmptyStructure>
|
25
|
+
#
|
26
|
+
policy_document = document.policy_document(@sid)
|
27
|
+
@s3.put_bucket_policy(
|
28
|
+
bucket: @bucket,
|
29
|
+
policy: policy_document,
|
30
|
+
)
|
31
|
+
puts "Add bucket policy to bucket #{@bucket}:"
|
32
|
+
puts policy_document
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class S3Secure::Policy
|
2
|
+
class List < Base
|
3
|
+
def run
|
4
|
+
buckets.each do |bucket|
|
5
|
+
@s3 = s3_regional_client(bucket)
|
6
|
+
puts "Policy for bucket #{bucket.color(:green)}"
|
7
|
+
policy = get_policy(bucket)
|
8
|
+
|
9
|
+
if policy
|
10
|
+
puts policy
|
11
|
+
else
|
12
|
+
puts "Bucket does not have a bucket policy"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_policy(bucket)
|
18
|
+
resp = @s3.get_bucket_policy(bucket: bucket)
|
19
|
+
data = JSON.load(resp.policy.read) # String
|
20
|
+
JSON.pretty_generate(data)
|
21
|
+
rescue Aws::S3::Errors::NoSuchBucketPolicy
|
22
|
+
end
|
23
|
+
|
24
|
+
# Useful when calling List outside of the list CLI
|
25
|
+
def set_s3(client)
|
26
|
+
@s3 = client
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class S3Secure::Policy
|
2
|
+
class Show < Base
|
3
|
+
def run
|
4
|
+
@s3 = s3_regional_client(@bucket)
|
5
|
+
|
6
|
+
list = S3Secure::Policy::List.new(@options)
|
7
|
+
list.set_s3(@s3)
|
8
|
+
|
9
|
+
policy = list.get_policy(@bucket)
|
10
|
+
if policy
|
11
|
+
puts "Bucket #{@bucket} is configured with this policy:"
|
12
|
+
puts policy
|
13
|
+
# puts policy.map(&:to_h)
|
14
|
+
else
|
15
|
+
puts "Bucket #{@bucket} is not configured bucket policy"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class S3Secure::Policy
|
2
|
+
class Unforce < Base
|
3
|
+
def initialize(options={})
|
4
|
+
super
|
5
|
+
@sid = options[:sid]
|
6
|
+
end
|
7
|
+
|
8
|
+
def run
|
9
|
+
@s3 = s3_regional_client(@bucket)
|
10
|
+
|
11
|
+
list = S3Secure::Policy::List.new(@options)
|
12
|
+
list.set_s3(@s3)
|
13
|
+
|
14
|
+
bucket_policy = list.get_policy(@bucket)
|
15
|
+
document = Document.new(@bucket, bucket_policy, remove: true)
|
16
|
+
if document.has?(@sid)
|
17
|
+
# Set encryption rules
|
18
|
+
# Ruby docs: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_bucket_policy-instance_method
|
19
|
+
# API docs: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ServerSideEncryptionByDefault.html
|
20
|
+
#
|
21
|
+
# put_bucket_policy returns #<struct Aws::EmptyStructure>
|
22
|
+
#
|
23
|
+
policy_document = document.policy_document(@sid)
|
24
|
+
|
25
|
+
if policy_document
|
26
|
+
@s3.put_bucket_policy(
|
27
|
+
bucket: @bucket,
|
28
|
+
policy: policy_document,
|
29
|
+
)
|
30
|
+
else
|
31
|
+
@s3.delete_bucket_policy(bucket: @bucket)
|
32
|
+
end
|
33
|
+
|
34
|
+
puts "Remove bucket policy to bucket #{@bucket}:"
|
35
|
+
puts policy_document if policy_document
|
36
|
+
else
|
37
|
+
puts "Bucket policy for #{@bucket} does not have ForceSSLOnlyAccess policy statement. Nothing to be done."
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/s3-secure.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "s3_secure/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "s3-secure"
|
8
|
+
spec.version = S3Secure::VERSION
|
9
|
+
spec.authors = ["Tung Nguyen"]
|
10
|
+
spec.email = ["tongueroo@gmail.com"]
|
11
|
+
spec.summary = "S3 Bucket security hardening tool"
|
12
|
+
spec.homepage = "https://github.com/tongueroo/s3-secure"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activesupport"
|
22
|
+
spec.add_dependency "aws-sdk-s3"
|
23
|
+
spec.add_dependency "memoist"
|
24
|
+
spec.add_dependency "rainbow"
|
25
|
+
spec.add_dependency "thor"
|
26
|
+
spec.add_dependency "zeitwerk"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler"
|
29
|
+
spec.add_development_dependency "byebug"
|
30
|
+
spec.add_development_dependency "cli_markdown"
|
31
|
+
spec.add_development_dependency "rake"
|
32
|
+
spec.add_development_dependency "rspec"
|
33
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
describe S3Secure::Policy::Checker do
|
2
|
+
subject { S3Secure::Policy::Checker.new(policy_json) }
|
3
|
+
|
4
|
+
describe "already has ForceSSLOnlyAccess" do
|
5
|
+
let(:policy_json) {
|
6
|
+
<<~JSON
|
7
|
+
{
|
8
|
+
"Version": "2012-10-17",
|
9
|
+
"Statement": [
|
10
|
+
{
|
11
|
+
"Sid": "ForceSSLOnlyAccess",
|
12
|
+
"Effect": "Deny",
|
13
|
+
"Principal": "*",
|
14
|
+
"Action": "s3:GetObject",
|
15
|
+
"Resource": "arn:aws:s3:::my-test-s3-secure-us-east-1/*",
|
16
|
+
"Condition": {
|
17
|
+
"Bool": {
|
18
|
+
"aws:SecureTransport": "false"
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
]
|
23
|
+
}
|
24
|
+
JSON
|
25
|
+
}
|
26
|
+
|
27
|
+
it "has?" do
|
28
|
+
result = subject.has?("ForceSSLOnlyAccess")
|
29
|
+
expect(result).to be true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "doesnt have ForceSSLOnlyAccess" do
|
34
|
+
let(:policy_json) {
|
35
|
+
<<~JSON
|
36
|
+
{
|
37
|
+
"Version": "2008-10-17",
|
38
|
+
"Id": "PolicyForCloudFrontPrivateContent",
|
39
|
+
"Statement": [
|
40
|
+
{
|
41
|
+
"Sid": "1",
|
42
|
+
"Effect": "Allow",
|
43
|
+
"Principal": {
|
44
|
+
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E27G6OAEXAMPLE"
|
45
|
+
},
|
46
|
+
"Action": "s3:GetObject",
|
47
|
+
"Resource": "arn:aws:s3:::test-fake-website/*"
|
48
|
+
}
|
49
|
+
]
|
50
|
+
}
|
51
|
+
JSON
|
52
|
+
}
|
53
|
+
|
54
|
+
it "has?" do
|
55
|
+
result = subject.has?("ForceSSLOnlyAccess")
|
56
|
+
expect(result).to be false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "empty policy" do
|
61
|
+
let(:policy_json) { nil }
|
62
|
+
|
63
|
+
it "has?" do
|
64
|
+
result = subject.has?("ForceSSLOnlyAccess")
|
65
|
+
expect(result).to be false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
describe S3Secure::Policy::Document::ForceSSLOnlyAccessRemove do
|
2
|
+
subject { S3Secure::Policy::Document::ForceSSLOnlyAccessRemove.new("my-bucket", policy_json) }
|
3
|
+
|
4
|
+
describe "already has ForceSSLOnlyAccess only" do
|
5
|
+
let(:policy_json) {
|
6
|
+
<<~JSON
|
7
|
+
{
|
8
|
+
"Version": "2012-10-17",
|
9
|
+
"Statement": [
|
10
|
+
{
|
11
|
+
"Sid": "ForceSSLOnlyAccess",
|
12
|
+
"Effect": "Deny",
|
13
|
+
"Principal": "*",
|
14
|
+
"Action": "s3:GetObject",
|
15
|
+
"Resource": "arn:aws:s3:::my-bucket/*",
|
16
|
+
"Condition": {
|
17
|
+
"Bool": {
|
18
|
+
"aws:SecureTransport": "false"
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
]
|
23
|
+
}
|
24
|
+
JSON
|
25
|
+
}
|
26
|
+
|
27
|
+
it "policy_document" do
|
28
|
+
result = subject.policy_document
|
29
|
+
expect(result).to be nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "already has ForceSSLOnlyAccess and another statement" do
|
34
|
+
let(:policy_json) {
|
35
|
+
<<~JSON
|
36
|
+
{
|
37
|
+
"Version": "2008-10-17",
|
38
|
+
"Id": "PolicyForCloudFrontPrivateContent",
|
39
|
+
"Statement": [
|
40
|
+
{
|
41
|
+
"Sid": "1",
|
42
|
+
"Effect": "Allow",
|
43
|
+
"Principal": {
|
44
|
+
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E27G6OAEXAMPLE"
|
45
|
+
},
|
46
|
+
"Action": "s3:GetObject",
|
47
|
+
"Resource": "arn:aws:s3:::test-fake-website/*"
|
48
|
+
},
|
49
|
+
{
|
50
|
+
"Sid": "ForceSSLOnlyAccess",
|
51
|
+
"Effect": "Deny",
|
52
|
+
"Principal": "*",
|
53
|
+
"Action": "s3:GetObject",
|
54
|
+
"Resource": "arn:aws:s3:::my-bucket/*",
|
55
|
+
"Condition": {
|
56
|
+
"Bool": {
|
57
|
+
"aws:SecureTransport": "false"
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
]
|
62
|
+
}
|
63
|
+
JSON
|
64
|
+
}
|
65
|
+
|
66
|
+
it "policy_document" do
|
67
|
+
result = JSON.pretty_generate(subject.policy_document)
|
68
|
+
expect(result).not_to include("ForceSSLOnlyAccess")
|
69
|
+
expect(result).to include("PolicyForCloudFrontPrivateContent")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "empty policy" do
|
74
|
+
let(:policy_json) { nil }
|
75
|
+
|
76
|
+
it "policy_document" do
|
77
|
+
result = subject.policy_document
|
78
|
+
expect(result).to be nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "doesnt have ForceSSLOnlyAccess" do
|
83
|
+
let(:policy_json) {
|
84
|
+
<<~JSON
|
85
|
+
{
|
86
|
+
"Version": "2008-10-17",
|
87
|
+
"Id": "PolicyForCloudFrontPrivateContent",
|
88
|
+
"Statement": [
|
89
|
+
{
|
90
|
+
"Sid": "1",
|
91
|
+
"Effect": "Allow",
|
92
|
+
"Principal": {
|
93
|
+
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E27G6OAEXAMPLE"
|
94
|
+
},
|
95
|
+
"Action": "s3:GetObject",
|
96
|
+
"Resource": "arn:aws:s3:::test-fake-website/*"
|
97
|
+
}
|
98
|
+
]
|
99
|
+
}
|
100
|
+
JSON
|
101
|
+
}
|
102
|
+
|
103
|
+
it "policy_document" do
|
104
|
+
expect { subject.policy_document }.to raise_error(RuntimeError)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
describe S3Secure::Policy::Document do
|
2
|
+
subject { S3Secure::Policy::Document.new("my-bucket", policy_json) }
|
3
|
+
|
4
|
+
describe "already has ForceSSLOnlyAccess" do
|
5
|
+
let(:policy_json) {
|
6
|
+
<<~JSON
|
7
|
+
{
|
8
|
+
"Version": "2012-10-17",
|
9
|
+
"Statement": [
|
10
|
+
{
|
11
|
+
"Sid": "ForceSSLOnlyAccess",
|
12
|
+
"Effect": "Deny",
|
13
|
+
"Principal": "*",
|
14
|
+
"Action": "s3:GetObject",
|
15
|
+
"Resource": "arn:aws:s3:::my-bucket/*",
|
16
|
+
"Condition": {
|
17
|
+
"Bool": {
|
18
|
+
"aws:SecureTransport": "false"
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
]
|
23
|
+
}
|
24
|
+
JSON
|
25
|
+
}
|
26
|
+
|
27
|
+
it "policy_document" do
|
28
|
+
result = subject.policy_document("ForceSSLOnlyAccess")
|
29
|
+
expect(result).to include("ForceSSLOnlyAccess")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "doesnt have ForceSSLOnlyAccess" do
|
34
|
+
let(:policy_json) {
|
35
|
+
<<~JSON
|
36
|
+
{
|
37
|
+
"Version": "2008-10-17",
|
38
|
+
"Id": "PolicyForCloudFrontPrivateContent",
|
39
|
+
"Statement": [
|
40
|
+
{
|
41
|
+
"Sid": "1",
|
42
|
+
"Effect": "Allow",
|
43
|
+
"Principal": {
|
44
|
+
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E27G6OAEXAMPLE"
|
45
|
+
},
|
46
|
+
"Action": "s3:GetObject",
|
47
|
+
"Resource": "arn:aws:s3:::test-fake-website/*"
|
48
|
+
}
|
49
|
+
]
|
50
|
+
}
|
51
|
+
JSON
|
52
|
+
}
|
53
|
+
|
54
|
+
it "policy_document" do
|
55
|
+
result = subject.policy_document("ForceSSLOnlyAccess")
|
56
|
+
expect(result).to include("ForceSSLOnlyAccess")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "empty policy" do
|
61
|
+
let(:policy_json) { nil }
|
62
|
+
|
63
|
+
it "policy_document" do
|
64
|
+
result = subject.policy_document("ForceSSLOnlyAccess")
|
65
|
+
expect(result).to include("ForceSSLOnlyAccess")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|