Repository: pydantic/pydantic

Issue:
SecretStr min_length, max_length
# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.4
            pydantic compiled: True
                 install path: /app/venv/lib/python3.7/site-packages/pydantic
               python version: 3.7.5 (default, Apr 19 2020, 20:18:17)  [GCC 9.2.1 20191008]
                     platform: Linux-5.3.0-51-generic-x86_64-with-Ubuntu-19.10-eoan
     optional deps. installed: ['typing-extensions', 'email-validator']
```
When the constraints min_length or max_length are set for a field of type SecretStr the erorr appear:
e.g.

```
class SecrTest(BaseModel):
    b: SecretStr = Field(None, max_length= 4)

On field "b" the following field constraints are set but not enforced: max_length. 
For more details see https://pydantic-docs.helpmanual.io/usage/schema/#unenforced-field-constraints
```






Discussion:
makes sense, thanks for reporting.

PR welcome to fix this.
hehe, 
tried to fix it indeed:
https://github.com/transfluxus/pydantic/tree/fix_secret_str_constraints

not sure if all things are propper. Maybe you have a look or shall I just make a pull request?
oh, however test_schema.test_secret_types fails because the schema changed. So ultimately SecretBytes should have the same constraints I guess?

Resolving patch:
diff --git a/pydantic/schema.py b/pydantic/schema.py
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -47,6 +47,8 @@
     ConstrainedList,
     ConstrainedSet,
     ConstrainedStr,
+    SecretBytes,
+    SecretStr,
     conbytes,
     condecimal,
     confloat,
@@ -831,7 +833,13 @@ def go(type_: Any) -> Type[Any]:
         attrs: Optional[Tuple[str, ...]] = None
         constraint_func: Optional[Callable[..., type]] = None
         if isinstance(type_, type):
-            if issubclass(type_, str) and not issubclass(type_, (EmailStr, AnyUrl, ConstrainedStr)):
+            if issubclass(type_, (SecretStr, SecretBytes)):
+                attrs = ('max_length', 'min_length')
+
+                def constraint_func(**kwargs: Any) -> Type[Any]:
+                    return type(type_.__name__, (type_,), kwargs)
+
+            elif issubclass(type_, str) and not issubclass(type_, (EmailStr, AnyUrl, ConstrainedStr)):
                 attrs = ('max_length', 'min_length', 'regex')
                 constraint_func = constr
             elif issubclass(type_, bytes):
diff --git a/pydantic/types.py b/pydantic/types.py
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -593,13 +593,24 @@ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
 
 
 class SecretStr:
+    min_length: OptionalInt = None
+    max_length: OptionalInt = None
+
     @classmethod
     def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
-        field_schema.update(type='string', writeOnly=True, format='password')
+        update_not_none(
+            field_schema,
+            type='string',
+            writeOnly=True,
+            format='password',
+            minLength=cls.min_length,
+            maxLength=cls.max_length,
+        )
 
     @classmethod
     def __get_validators__(cls) -> 'CallableGenerator':
         yield cls.validate
+        yield constr_length_validator
 
     @classmethod
     def validate(cls, value: Any) -> 'SecretStr':
@@ -620,6 +631,9 @@ def __str__(self) -> str:
     def __eq__(self, other: Any) -> bool:
         return isinstance(other, SecretStr) and self.get_secret_value() == other.get_secret_value()
 
+    def __len__(self) -> int:
+        return len(self._secret_value)
+
     def display(self) -> str:
         warnings.warn('`secret_str.display()` is deprecated, use `str(secret_str)` instead', DeprecationWarning)
         return str(self)
@@ -629,13 +643,24 @@ def get_secret_value(self) -> str:
 
 
 class SecretBytes:
+    min_length: OptionalInt = None
+    max_length: OptionalInt = None
+
     @classmethod
     def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
-        field_schema.update(type='string', writeOnly=True, format='password')
+        update_not_none(
+            field_schema,
+            type='string',
+            writeOnly=True,
+            format='password',
+            minLength=cls.min_length,
+            maxLength=cls.max_length,
+        )
 
     @classmethod
     def __get_validators__(cls) -> 'CallableGenerator':
         yield cls.validate
+        yield constr_length_validator
 
     @classmethod
     def validate(cls, value: Any) -> 'SecretBytes':
@@ -656,6 +681,9 @@ def __str__(self) -> str:
     def __eq__(self, other: Any) -> bool:
         return isinstance(other, SecretBytes) and self.get_secret_value() == other.get_secret_value()
 
+    def __len__(self) -> int:
+        return len(self._secret_value)
+
     def display(self) -> str:
         warnings.warn('`secret_bytes.display()` is deprecated, use `str(secret_bytes)` instead', DeprecationWarning)
         return str(self)


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -2124,6 +2124,38 @@ class Foobar(BaseModel):
     assert exc_info.value.errors() == [{'loc': ('password',), 'msg': 'str type expected', 'type': 'type_error.str'}]
 
 
+def test_secretstr_min_max_length():
+    class Foobar(BaseModel):
+        password: SecretStr = Field(min_length=6, max_length=10)
+
+    with pytest.raises(ValidationError) as exc_info:
+        Foobar(password='')
+
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('password',),
+            'msg': 'ensure this value has at least 6 characters',
+            'type': 'value_error.any_str.min_length',
+            'ctx': {'limit_value': 6},
+        }
+    ]
+
+    with pytest.raises(ValidationError) as exc_info:
+        Foobar(password='1' * 20)
+
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('password',),
+            'msg': 'ensure this value has at most 10 characters',
+            'type': 'value_error.any_str.max_length',
+            'ctx': {'limit_value': 10},
+        }
+    ]
+
+    value = '1' * 8
+    assert Foobar(password=value).password.get_secret_value() == value
+
+
 def test_secretbytes():
     class Foobar(BaseModel):
         password: SecretBytes
@@ -2180,6 +2212,63 @@ class Foobar(BaseModel):
     assert exc_info.value.errors() == [{'loc': ('password',), 'msg': 'byte type expected', 'type': 'type_error.bytes'}]
 
 
+def test_secretbytes_min_max_length():
+    class Foobar(BaseModel):
+        password: SecretBytes = Field(min_length=6, max_length=10)
+
+    with pytest.raises(ValidationError) as exc_info:
+        Foobar(password=b'')
+
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('password',),
+            'msg': 'ensure this value has at least 6 characters',
+            'type': 'value_error.any_str.min_length',
+            'ctx': {'limit_value': 6},
+        }
+    ]
+
+    with pytest.raises(ValidationError) as exc_info:
+        Foobar(password=b'1' * 20)
+
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('password',),
+            'msg': 'ensure this value has at most 10 characters',
+            'type': 'value_error.any_str.max_length',
+            'ctx': {'limit_value': 10},
+        }
+    ]
+
+    value = b'1' * 8
+    assert Foobar(password=value).password.get_secret_value() == value
+
+
+@pytest.mark.parametrize('secret_cls', [SecretStr, SecretBytes])
+@pytest.mark.parametrize(
+    'field_kw,schema_kw',
+    [
+        [{}, {}],
+        [{'min_length': 6}, {'minLength': 6}],
+        [{'max_length': 10}, {'maxLength': 10}],
+        [{'min_length': 6, 'max_length': 10}, {'minLength': 6, 'maxLength': 10}],
+    ],
+    ids=['no-constrains', 'min-constraint', 'max-constraint', 'min-max-constraints'],
+)
+def test_secrets_schema(secret_cls, field_kw, schema_kw):
+    class Foobar(BaseModel):
+        password: secret_cls = Field(**field_kw)
+
+    assert Foobar.schema() == {
+        'title': 'Foobar',
+        'type': 'object',
+        'properties': {
+            'password': {'title': 'Password', 'type': 'string', 'writeOnly': True, 'format': 'password', **schema_kw}
+        },
+        'required': ['password'],
+    }
+
+
 def test_generic_without_params():
     class Model(BaseModel):
         generic_list: List