Passed
Push — master ( 6652ed...2f17b4 )
by Vinicius
03:43 queued 16s
created

kytos.core.user.hashing()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nop 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
"""User authentification """
2
# pylint: disable=no-name-in-module, no-self-argument
3
import hashlib
4
import os
5
from datetime import datetime
6
from typing import Literal, Optional
7
8
from pydantic import (BaseModel, EmailStr, Field, ValidationInfo,
9
                      field_validator)
10
from typing_extensions import Annotated
11
12
13
class DocumentBaseModel(BaseModel):
14
    """DocumentBaseModel"""
15
16
    id: str = Field(None, alias="_id")
17
    inserted_at: Optional[datetime] = None
18
    updated_at: Optional[datetime] = None
19
    deleted_at: Optional[datetime] = None
20
21
    def model_dump(self, **kwargs) -> dict:
22
        """Model to dict."""
23
        values = super().model_dump(**kwargs)
24
        if "id" in values and values["id"]:
25
            values["_id"] = values["id"]
26
        if "exclude" in kwargs and "_id" in kwargs["exclude"]:
27
            values.pop("_id")
28
        return values
29
30
31
def hashing(password: bytes, values: dict) -> str:
32
    """Hash password and return it as string"""
33
    return hashlib.scrypt(password=password, salt=values['salt'],
34
                          n=values['n'], r=values['r'],
35
                          p=values['p']).hex()
36
37
38
def validate_password(password: str, values: ValidationInfo):
39
    """Check if password has at least a letter and a number"""
40
    upper = False
41
    lower = False
42
    number = False
43
    for char in password:
44
        if char.isupper():
45
            upper = True
46
        if char.isnumeric():
47
            number = True
48
        if char.islower():
49
            lower = True
50
        if number and upper and lower:
51
            return hashing(password.encode(), values.data['hash'].model_dump())
52
    raise ValueError('value should contain ' +
53
                     'minimun 8 characters, ' +
54
                     'at least one upper case character, ' +
55
                     'at least 1 numeric character [0-9]')
56
57
58
class HashSubDoc(BaseModel):
59
    """HashSubDoc. Parameters for hash.scrypt function"""
60
    salt: bytes = Field(default=None, validate_default=True)
61
    n: int = 8192
62
    r: int = 8
63
    p: int = 1
64
65
    @field_validator('salt', mode='before')
66
    @classmethod
67
    def create_salt(cls, salt):
68
        """Create random salt value"""
69
        return salt or os.urandom(16)
70
71
72
class UserDoc(DocumentBaseModel):
73
    """UserDocumentModel."""
74
75
    username: str = Field(
76
        min_length=1, max_length=64, pattern=r'^[a-zA-Z0-9_-]+$'
77
    )
78
    hash: HashSubDoc
79
    state: Literal['active', 'inactive'] = 'active'
80
    email: EmailStr
81
    password: str = Field(min_length=8, max_length=64)
82
83
    _validate_password = field_validator('password')(validate_password)
84
85
    @staticmethod
86
    def projection() -> dict:
87
        """Base model for projection."""
88
        return {
89
            "_id": 0,
90
            "username": 1,
91
            "email": 1,
92
            'password': 1,
93
            'hash': 1,
94
            'state': 1,
95
            'inserted_at': 1,
96
            'updated_at': 1,
97
            'deleted_at': 1
98
        }
99
100
    @staticmethod
101
    def projection_nopw() -> dict:
102
        """Model for projection without password"""
103
        return {
104
            "_id": 0,
105
            "username": 1,
106
            "email": 1,
107
            'state': 1,
108
            'inserted_at': 1,
109
            'updated_at': 1,
110
            'deleted_at': 1
111
        }
112
113
114
class UserDocUpdate(DocumentBaseModel):
115
    "UserDocUpdate use to validate data before updating"
116
117
    username: Optional[Annotated[str,
118
                                 Field(min_length=1, max_length=64,
119
                                       pattern=r'^[a-zA-Z0-9_-]+$')]] = None
120
    email: Optional[EmailStr] = None
121
    hash: Optional[HashSubDoc] = None
122
    password: Optional[Annotated[str,
123
                                 Field(min_length=8, max_length=64)]] = None
124
125
    _validate_password = field_validator('password')(validate_password)
126