password_policy 0.1
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +111 -0
- data/Rakefile +10 -0
- data/lib/password_policy.rb +82 -0
- data/lib/password_policy/rules.rb +52 -0
- data/lib/password_policy/version.rb +3 -0
- data/password_policy.gemspec +17 -0
- data/test/test_password_policy.rb +25 -0
- data/test/test_rule_definitions.rb +105 -0
- metadata +58 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2012 Craig Russell
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Password Policy #
|
|
2
|
+
|
|
3
|
+
A password policy class for Ruby
|
|
4
|
+
|
|
5
|
+
**Author** Craig Russell [craig@craig-russell.co.uk](mailto:craig@craig-russell.co.uk)
|
|
6
|
+
**Version** 0.1
|
|
7
|
+
|
|
8
|
+
Password Policy is a library that makes it easy to implement a password format policy on your application. It includes functions to return password validation errors.
|
|
9
|
+
## Download ##
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Usage ##
|
|
13
|
+
|
|
14
|
+
### Getting Started ###
|
|
15
|
+
|
|
16
|
+
Using Password Policy on your site is simple.
|
|
17
|
+
|
|
18
|
+
1. Install the Gem
|
|
19
|
+
|
|
20
|
+
$ gem install password_policy
|
|
21
|
+
|
|
22
|
+
2. Include the library
|
|
23
|
+
|
|
24
|
+
> require 'password_policy'
|
|
25
|
+
|
|
26
|
+
2. Create the Password Policy object
|
|
27
|
+
|
|
28
|
+
> @policy = PasswordPolicy.new
|
|
29
|
+
|
|
30
|
+
3. Define the policy rules
|
|
31
|
+
|
|
32
|
+
> @policy.min_length = 8;
|
|
33
|
+
> @policy.max_length = 64;
|
|
34
|
+
|
|
35
|
+
4. Validate a password
|
|
36
|
+
|
|
37
|
+
puts "Password OK!" if @policy.validate 'passw0rd'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Advanced ##
|
|
41
|
+
|
|
42
|
+
### Separate Policy Rules ###
|
|
43
|
+
|
|
44
|
+
You can define your policy rules separately as a Hash and pass this to the constructor, rather than defining rules on the object. Hash keys should be rule identifiers, values should be the correct type. This is useful if you want to define your policy rules is a separate configuration file.
|
|
45
|
+
|
|
46
|
+
@rules = {
|
|
47
|
+
:min_length => 8,
|
|
48
|
+
:max_length => 64
|
|
49
|
+
}
|
|
50
|
+
@policy = PasswordPolicy.new @rules
|
|
51
|
+
|
|
52
|
+
### Password Validation Errors ###
|
|
53
|
+
|
|
54
|
+
After validating the password, any errors can be retrieved as an array of strings.
|
|
55
|
+
|
|
56
|
+
@policy.errors.each do |error|
|
|
57
|
+
puts error
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
## Policy Rules ##
|
|
61
|
+
|
|
62
|
+
A Password Policy has several rules which can be configured, these are detailed below.
|
|
63
|
+
|
|
64
|
+
Policy rules have different types, a rule will ignore any attempt to set its value to an incorrect type. Please refer to the documentation below.
|
|
65
|
+
|
|
66
|
+
**IMPORTANT** It is possible to define conflicting rules in a policy, which would make it impossible to set a password.
|
|
67
|
+
|
|
68
|
+
@rules = {
|
|
69
|
+
:min_length => 20,
|
|
70
|
+
:max_length => 10
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
### Minimum Length ###
|
|
74
|
+
|
|
75
|
+
The minimum length of a password
|
|
76
|
+
|
|
77
|
+
**Identifier** `min_length`
|
|
78
|
+
|
|
79
|
+
@policy.min_length = 10;
|
|
80
|
+
|
|
81
|
+
### Maximum Length ###
|
|
82
|
+
|
|
83
|
+
The maximum length of a password
|
|
84
|
+
|
|
85
|
+
**Identifier** `max_length`
|
|
86
|
+
|
|
87
|
+
@policy.max_length = 64;
|
|
88
|
+
|
|
89
|
+
### Minimum Lower-case Characters ###
|
|
90
|
+
|
|
91
|
+
The minimum number of lower-case characters allowed in a password
|
|
92
|
+
|
|
93
|
+
**Identifier** `min_lowercase_chars`
|
|
94
|
+
|
|
95
|
+
@policy.min_lowercase_chars = 1;
|
|
96
|
+
|
|
97
|
+
### Minimum Uppercase Characters ###
|
|
98
|
+
|
|
99
|
+
The minimum number of uppercase characters allowed in a password
|
|
100
|
+
|
|
101
|
+
**Identifier** `min_uppercase_chars`
|
|
102
|
+
|
|
103
|
+
@policy.min_uppercase_chars = 1;
|
|
104
|
+
|
|
105
|
+
### Minimun Numeric Characters ###
|
|
106
|
+
|
|
107
|
+
The minimum number of numeric characters allowed in a password
|
|
108
|
+
|
|
109
|
+
**Identifier** `min_numeric_chars`
|
|
110
|
+
|
|
111
|
+
@policy.min_numeric_chars = 1;
|
data/Rakefile
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require_relative "password_policy/version"
|
|
2
|
+
require_relative "password_policy/rules"
|
|
3
|
+
|
|
4
|
+
class PasswordPolicy
|
|
5
|
+
|
|
6
|
+
# Constructor
|
|
7
|
+
#
|
|
8
|
+
# == Parameters:
|
|
9
|
+
# rules::
|
|
10
|
+
# A Hash of rule values defining the policy
|
|
11
|
+
#
|
|
12
|
+
# == Returns:
|
|
13
|
+
# A PasswordPolicy object
|
|
14
|
+
def initialize(rules = {})
|
|
15
|
+
|
|
16
|
+
# Initialize error message and rule data structures
|
|
17
|
+
@errors = []
|
|
18
|
+
@rules = {}
|
|
19
|
+
|
|
20
|
+
# Define rules
|
|
21
|
+
rule_definitions
|
|
22
|
+
|
|
23
|
+
# Define accessors for rule values
|
|
24
|
+
@rules.each do |id, rule|
|
|
25
|
+
define_singleton_method(id) { rule[:value] }
|
|
26
|
+
define_singleton_method("#{id}=") { |val| rule[:value] = val }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Merge provided rules values with defaults
|
|
30
|
+
rules.each do |rule, val|
|
|
31
|
+
@rules[rule.to_sym][:value] = val
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Validate a password against the password policy
|
|
36
|
+
#
|
|
37
|
+
# == Parameters:
|
|
38
|
+
# password:
|
|
39
|
+
# A password String to be validated
|
|
40
|
+
#
|
|
41
|
+
# == Returns:
|
|
42
|
+
# True if the password is validated
|
|
43
|
+
# false otherwise
|
|
44
|
+
#
|
|
45
|
+
def validate(password)
|
|
46
|
+
# Reset errors
|
|
47
|
+
@errors = []
|
|
48
|
+
# Test password against rules
|
|
49
|
+
@rules.each_value do |rule|
|
|
50
|
+
@errors << vsub(rule[:error_msg], rule[:value]) unless rule[:test].call(password)
|
|
51
|
+
end
|
|
52
|
+
# Password validated if no errors
|
|
53
|
+
@errors.length == 0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get a list of failed validation errors
|
|
57
|
+
#
|
|
58
|
+
# == Returns:
|
|
59
|
+
# An Array of password error Strings
|
|
60
|
+
#
|
|
61
|
+
def errors
|
|
62
|
+
@errors
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Globally substitute #VAL# for val in string
|
|
68
|
+
#
|
|
69
|
+
# == Parameters:
|
|
70
|
+
# string:
|
|
71
|
+
# The host String
|
|
72
|
+
# val:
|
|
73
|
+
# The value to be substituted in
|
|
74
|
+
#
|
|
75
|
+
# == Returns:
|
|
76
|
+
# The host Sting with all instances of #VAL#
|
|
77
|
+
# replaced with val:
|
|
78
|
+
#
|
|
79
|
+
def vsub(string, val)
|
|
80
|
+
string.gsub('#VAL#', val.to_s)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class PasswordPolicy
|
|
2
|
+
|
|
3
|
+
def rule_definitions
|
|
4
|
+
# Rule definitions are stored in the Hash @rules
|
|
5
|
+
# Hash key describes rule and is used as the accessor
|
|
6
|
+
# :value Default value for rule (can be overridden in constructor)
|
|
7
|
+
# :error_msg Message returned if validation fails
|
|
8
|
+
# :test Proc returning true if password validates against rule
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@rules[:min_length] = {
|
|
12
|
+
:value => 8,
|
|
13
|
+
:error_msg => 'Password must be more than #VAL# characters',
|
|
14
|
+
:test => proc do |password|
|
|
15
|
+
password.length >= @rules[:min_length][:value]
|
|
16
|
+
end
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@rules[:max_length] = {
|
|
20
|
+
:value => 64,
|
|
21
|
+
:error_msg => 'Password must be less than #VAL# characters',
|
|
22
|
+
:test => proc do |password|
|
|
23
|
+
password.length <= @rules[:max_length][:value]
|
|
24
|
+
end
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@rules[:min_lowercase_chars] = {
|
|
28
|
+
:value => 0,
|
|
29
|
+
:error_msg => 'Password must contain at least #VAL# lowercase characters',
|
|
30
|
+
:test => proc do |password|
|
|
31
|
+
password.scan(/[a-z]/).size >= @rules[:min_lowercase_chars][:value]
|
|
32
|
+
end
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@rules[:min_uppercase_chars] = {
|
|
36
|
+
:value => 0,
|
|
37
|
+
:error_msg => 'Password must contain at least #VAL# uppercase characters',
|
|
38
|
+
:test => proc do |password|
|
|
39
|
+
password.scan(/[A-Z]/).size >= @rules[:min_uppercase_chars][:value]
|
|
40
|
+
end
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@rules[:min_numeric_chars] = {
|
|
44
|
+
:value => 0,
|
|
45
|
+
:error_msg => 'Password must contain at least #VAL# numeric characters',
|
|
46
|
+
:test => proc do |password|
|
|
47
|
+
password.scan(/[0-9]/).size >= @rules[:min_numeric_chars][:value]
|
|
48
|
+
end
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
require File.expand_path('../lib/password_policy/version', __FILE__)
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.authors = ["Craig Russell"]
|
|
6
|
+
gem.email = ["craig@craig-russell.co.uk"]
|
|
7
|
+
gem.description = %q{A simple password policy enforcer}
|
|
8
|
+
gem.summary = %q{Password Policy}
|
|
9
|
+
gem.homepage = ""
|
|
10
|
+
|
|
11
|
+
gem.files = `git ls-files`.split($\)
|
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
14
|
+
gem.name = "password_policy"
|
|
15
|
+
gem.require_paths = ["lib"]
|
|
16
|
+
gem.version = PasswordPolicy::VERSION
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require File.expand_path('../../lib/password_policy', __FILE__)
|
|
3
|
+
|
|
4
|
+
class PasswordPolicyTests < Test::Unit::TestCase
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
@pp = PasswordPolicy.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_password_policy_object
|
|
11
|
+
assert_not_nil PasswordPolicy
|
|
12
|
+
assert_equal PasswordPolicy, @pp.class
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_error_message_method
|
|
16
|
+
assert @pp.respond_to? :errors
|
|
17
|
+
assert_equal Array, @pp.errors.class
|
|
18
|
+
assert_equal [], @pp.errors
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_validation_method
|
|
22
|
+
assert @pp.respond_to? :validate
|
|
23
|
+
assert @pp.validate('').is_a?(TrueClass) || @pp.validate('').is_a?(FalseClass)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require File.expand_path('../../lib/password_policy', __FILE__)
|
|
3
|
+
|
|
4
|
+
class PasswordPolicyRulesTests < Test::Unit::TestCase
|
|
5
|
+
|
|
6
|
+
def test_min_length_rule
|
|
7
|
+
assert pp1 = PasswordPolicy.new
|
|
8
|
+
assert pp2 = PasswordPolicy.new(:min_length => 6)
|
|
9
|
+
|
|
10
|
+
assert_respond_to pp1, :min_length
|
|
11
|
+
assert_respond_to pp1, :min_length=
|
|
12
|
+
|
|
13
|
+
assert_equal 8, pp1.min_length
|
|
14
|
+
assert_equal 6, pp2.min_length
|
|
15
|
+
|
|
16
|
+
assert_equal false, pp1.validate('x' * 7)
|
|
17
|
+
assert_equal true, pp1.validate('x' * 8)
|
|
18
|
+
|
|
19
|
+
assert pp1.min_length = 10
|
|
20
|
+
assert_equal 10, pp1.min_length
|
|
21
|
+
|
|
22
|
+
assert_equal false, pp1.validate('x' * 9)
|
|
23
|
+
assert_equal true, pp1.validate('x' * 10)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_max_length_rule
|
|
27
|
+
assert pp1 = PasswordPolicy.new
|
|
28
|
+
assert pp2 = PasswordPolicy.new(:max_length => 10)
|
|
29
|
+
|
|
30
|
+
assert_respond_to pp1, :max_length
|
|
31
|
+
assert_respond_to pp1, :max_length=
|
|
32
|
+
|
|
33
|
+
assert_equal 64, pp1.max_length
|
|
34
|
+
assert_equal 10, pp2.max_length
|
|
35
|
+
|
|
36
|
+
assert_equal false, pp1.validate('x' * 65)
|
|
37
|
+
assert_equal true, pp1.validate('x' * 64)
|
|
38
|
+
|
|
39
|
+
assert pp1.max_length = 32
|
|
40
|
+
assert_equal 32, pp1.max_length
|
|
41
|
+
|
|
42
|
+
assert_equal false, pp1.validate('x' * 33)
|
|
43
|
+
assert_equal true, pp1.validate('x' * 32)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_min_lowercase_chars_rule
|
|
47
|
+
assert pp1 = PasswordPolicy.new
|
|
48
|
+
assert pp2 = PasswordPolicy.new(:min_lowercase_chars => 1)
|
|
49
|
+
|
|
50
|
+
assert_respond_to pp1, :min_lowercase_chars
|
|
51
|
+
assert_respond_to pp1, :min_lowercase_chars=
|
|
52
|
+
|
|
53
|
+
assert_equal 0, pp1.min_lowercase_chars
|
|
54
|
+
assert_equal 1, pp2.min_lowercase_chars
|
|
55
|
+
|
|
56
|
+
assert_equal false, pp2.validate('ABCDEFGHI')
|
|
57
|
+
assert_equal true, pp2.validate('AaBCDEFGHI')
|
|
58
|
+
|
|
59
|
+
assert pp1.min_lowercase_chars = 3
|
|
60
|
+
assert_equal 3, pp1.min_lowercase_chars
|
|
61
|
+
|
|
62
|
+
assert_equal false, pp1.validate('AaBbCDEFGHI')
|
|
63
|
+
assert_equal true, pp1.validate('AaBbCcDEFGHI')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_min_uppercase_chars_rule
|
|
67
|
+
assert pp1 = PasswordPolicy.new
|
|
68
|
+
assert pp2 = PasswordPolicy.new(:min_uppercase_chars => 1)
|
|
69
|
+
|
|
70
|
+
assert_respond_to pp1, :min_uppercase_chars
|
|
71
|
+
assert_respond_to pp1, :min_uppercase_chars=
|
|
72
|
+
|
|
73
|
+
assert_equal 0, pp1.min_uppercase_chars
|
|
74
|
+
assert_equal 1, pp2.min_uppercase_chars
|
|
75
|
+
|
|
76
|
+
assert_equal false, pp2.validate('abcdefghi')
|
|
77
|
+
assert_equal true, pp2.validate('aAbcdefghi')
|
|
78
|
+
|
|
79
|
+
assert pp1.min_uppercase_chars = 3
|
|
80
|
+
assert_equal 3, pp1.min_uppercase_chars
|
|
81
|
+
|
|
82
|
+
assert_equal false, pp1.validate('aAbBcdefghi')
|
|
83
|
+
assert_equal true, pp1.validate('aAbBcCdefghi')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_min_numeric_chars_rule
|
|
87
|
+
assert pp1 = PasswordPolicy.new
|
|
88
|
+
assert pp2 = PasswordPolicy.new(:min_numeric_chars => 1)
|
|
89
|
+
|
|
90
|
+
assert_respond_to pp1, :min_numeric_chars
|
|
91
|
+
assert_respond_to pp1, :min_numeric_chars=
|
|
92
|
+
|
|
93
|
+
assert_equal 0, pp1.min_numeric_chars
|
|
94
|
+
assert_equal 1, pp2.min_numeric_chars
|
|
95
|
+
|
|
96
|
+
assert_equal false, pp2.validate('abcdefghi')
|
|
97
|
+
assert_equal true, pp2.validate('abcdefghi1')
|
|
98
|
+
|
|
99
|
+
assert pp1.min_numeric_chars = 3
|
|
100
|
+
assert_equal 3, pp1.min_numeric_chars
|
|
101
|
+
|
|
102
|
+
assert_equal false, pp1.validate('abcdefghi1')
|
|
103
|
+
assert_equal true, pp1.validate('abcdefghi123')
|
|
104
|
+
end
|
|
105
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: password_policy
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: '0.1'
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Craig Russell
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-04-13 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: A simple password policy enforcer
|
|
15
|
+
email:
|
|
16
|
+
- craig@craig-russell.co.uk
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- .gitignore
|
|
22
|
+
- Gemfile
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- Rakefile
|
|
26
|
+
- lib/password_policy.rb
|
|
27
|
+
- lib/password_policy/rules.rb
|
|
28
|
+
- lib/password_policy/version.rb
|
|
29
|
+
- password_policy.gemspec
|
|
30
|
+
- test/test_password_policy.rb
|
|
31
|
+
- test/test_rule_definitions.rb
|
|
32
|
+
homepage: ''
|
|
33
|
+
licenses: []
|
|
34
|
+
post_install_message:
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
none: false
|
|
40
|
+
requirements:
|
|
41
|
+
- - ! '>='
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
none: false
|
|
46
|
+
requirements:
|
|
47
|
+
- - ! '>='
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubyforge_project:
|
|
52
|
+
rubygems_version: 1.8.21
|
|
53
|
+
signing_key:
|
|
54
|
+
specification_version: 3
|
|
55
|
+
summary: Password Policy
|
|
56
|
+
test_files:
|
|
57
|
+
- test/test_password_policy.rb
|
|
58
|
+
- test/test_rule_definitions.rb
|