Added an option to enable a password check against a Banned-Password List (#7315)

* Added an option to enable a password check against a Banned-Password-List

* Update services/frontend/README.md

Co-authored-by: Martin <github@diemattels.at>

Co-authored-by: Edith Parzefall <edith_parzefall@gmx.de>

---------

Co-authored-by: Roman Perekhod <rperekhod@owncloud.com>
Co-authored-by: Martin <github@diemattels.at>
Co-authored-by: Edith Parzefall <edith_parzefall@gmx.de>
This commit is contained in:
Roman Perekhod
2023-09-21 12:31:48 +02:00
committed by GitHub
parent 862ad4d0f2
commit 2e7b423dcc
14 changed files with 142 additions and 58 deletions

View File

@@ -0,0 +1,6 @@
Enhancement: Add the Banned Passwords List
Added an option to enable a password check against a banned passwords list OCIS-3809
https://github.com/cs3org/reva/pull/4197
https://github.com/owncloud/ocis/pull/7314

2
go.mod
View File

@@ -13,7 +13,7 @@ require (
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/coreos/go-oidc/v3 v3.6.0
github.com/cs3org/go-cs3apis v0.0.0-20230516150832-730ac860c71d
github.com/cs3org/reva/v2 v2.16.1-0.20230915081009-843fe781fbf8
github.com/cs3org/reva/v2 v2.16.1-0.20230921092447-6caa589a0ae8
github.com/disintegration/imaging v1.6.2
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
github.com/egirna/icap-client v0.1.1

4
go.sum
View File

@@ -1013,8 +1013,8 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
github.com/crewjam/saml v0.4.13 h1:TYHggH/hwP7eArqiXSJUvtOPNzQDyQ7vwmwEqlFWhMc=
github.com/crewjam/saml v0.4.13/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
github.com/cs3org/reva/v2 v2.16.1-0.20230915081009-843fe781fbf8 h1:7aj/OEZ6NbpB3nSKRJUlp2l0AT0j5tGP0nynLZS8UpU=
github.com/cs3org/reva/v2 v2.16.1-0.20230915081009-843fe781fbf8/go.mod h1:RvhuweTFqzezjUFU0SIdTXakrEx9vJlMvQ7znPXSP1g=
github.com/cs3org/reva/v2 v2.16.1-0.20230921092447-6caa589a0ae8 h1:IEziHvFgy6EIGkGJb3lK/TQNCzBkd4oNe5YDBMcbS1c=
github.com/cs3org/reva/v2 v2.16.1-0.20230921092447-6caa589a0ae8/go.mod h1:RvhuweTFqzezjUFU0SIdTXakrEx9vJlMvQ7znPXSP1g=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

View File

@@ -74,13 +74,17 @@ Generally, a password can contain any UTF-8 characters, however some characters
Note that a password can have a maximum length of **72 bytes**. Depending on the alphabet used, a character is encoded by 1 to 4 bytes, defining the maximum length of a password indirectly. While US-ASCII will only need one byte, Latin alphabets and also Greek or Cyrillic ones need two bytes. Three bytes are needed for characters in Chinese, Japanese and Korean etc.
- `FRONTEND_PASSWORD_POLICIES_MIN_CHARACTERS`
The validation against the banned passwords list can be configured via a text file with words separated by new lines. If a user tries to set a password listed in the banned passwords list, the password can not be used (is invalid) even if the other mandatory criteria are passed. The admin can define the path of the banned passwords list file. If the file doesn't exist in a location, Infinite Scale tries to load a file from the `OCIS_CONFIG_DIR/FRONTEND_PASSWORD_POLICY_BANNED_PASSWORDS_LIST`. An option will be enabled when the file has been loaded successfully.
- `FRONTEND_PASSWORD_POLICY_MIN_CHARACTERS`
Define the minimum password length.
- `FRONTEND_PASSWORD_POLICIES_MIN_LOWERCASE_CHARACTERS`
- `FRONTEND_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS`
Define the minimum number of uppercase letters.
- `FRONTEND_PASSWORD_POLICIES_MIN_UPPERCASE_CHARACTERS`
- `FRONTEND_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS`
Define the minimum number of lowercase letters.
- `FRONTEND_PASSWORD_POLICIES_MIN_DIGITS`
- `FRONTEND_PASSWORD_POLICY_MIN_DIGITS`
Define the minimum number of digits.
- `FRONTEND_PASSWORD_POLICIES_MIN_SPECIAL_CHARACTERS`
- `FRONTEND_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS`
Define the minimum number of special characters.
- `FRONTEND_PASSWORD_POLICY_BANNED_PASSWORDS_LIST`
Define the path to the banned password list file.

View File

@@ -42,7 +42,7 @@ func Server(cfg *config.Config) *cli.Command {
defer cancel()
rCfg, err := revaconfig.FrontendConfigFromStruct(cfg)
rCfg, err := revaconfig.FrontendConfigFromStruct(cfg, logger)
if err != nil {
return err
}

View File

@@ -176,9 +176,10 @@ type ServiceAccount struct {
// PasswordPolicy configures reva password policy
type PasswordPolicy struct {
MinCharacters int `yaml:"min_characters,omitempty" env:"FRONTEND_PASSWORD_POLICY_MIN_CHARACTERS" desc:"Define the minimum password length. Defaults to 0 if not set."`
MinLowerCaseCharacters int `yaml:"min_lowercase_characters" env:"FRONTEND_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS" desc:"Define the minimum number of uppercase letters. Defaults to 0 if not set."`
MinUpperCaseCharacters int `yaml:"min_uppercase_characters" env:"FRONTEND_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS" desc:"Define the minimum number of lowercase letters. Defaults to 0 if not set."`
MinDigits int `yaml:"min_digits" env:"FRONTEND_PASSWORD_POLICY_MIN_DIGITS" desc:"Define the minimum number of digits. Defaults to 0 if not set."`
MinSpecialCharacters int `yaml:"min_special_characters" env:"FRONTEND_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS" desc:"Define the minimum number of characters from the special characters list to be present. Defaults to 0 if not set."`
MinCharacters int `yaml:"min_characters,omitempty" env:"FRONTEND_PASSWORD_POLICY_MIN_CHARACTERS" desc:"Define the minimum password length. Defaults to 0 if not set."`
MinLowerCaseCharacters int `yaml:"min_lowercase_characters" env:"FRONTEND_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS" desc:"Define the minimum number of uppercase letters. Defaults to 0 if not set."`
MinUpperCaseCharacters int `yaml:"min_uppercase_characters" env:"FRONTEND_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS" desc:"Define the minimum number of lowercase letters. Defaults to 0 if not set."`
MinDigits int `yaml:"min_digits" env:"FRONTEND_PASSWORD_POLICY_MIN_DIGITS" desc:"Define the minimum number of digits. Defaults to 0 if not set."`
MinSpecialCharacters int `yaml:"min_special_characters" env:"FRONTEND_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS" desc:"Define the minimum number of characters from the special characters list to be present. Defaults to 0 if not set."`
BannedPasswordsList string `yaml:"banned_passwords_list" env:"FRONTEND_PASSWORD_POLICY_BANNED_PASSWORDS_LIST" desc:"Path to the 'banned passwords list' file. See the documentation for more details."`
}

View File

@@ -1,17 +1,23 @@
package revaconfig
import (
"bufio"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"time"
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
"github.com/owncloud/ocis/v2/services/frontend/pkg/config"
)
// FrontendConfigFromStruct will adapt an oCIS config struct into a reva mapstructure to start a reva service.
func FrontendConfigFromStruct(cfg *config.Config) (map[string]interface{}, error) {
func FrontendConfigFromStruct(cfg *config.Config, logger log.Logger) (map[string]interface{}, error) {
webURL, err := url.Parse(cfg.PublicURL)
if err != nil {
return nil, err
@@ -19,6 +25,16 @@ func FrontendConfigFromStruct(cfg *config.Config) (map[string]interface{}, error
webURL.Path = path.Join(webURL.Path, "external")
webOpenInAppURL := webURL.String()
var bannedPasswordsList map[string]struct{}
if cfg.PasswordPolicy.BannedPasswordsList != "" {
bannedPasswordsList, err = readMultilineFile(cfg.PasswordPolicy.BannedPasswordsList)
if err != nil {
err = fmt.Errorf("failed to load the banned passwords from a file %s: %w", cfg.PasswordPolicy.BannedPasswordsList, err)
logger.Err(err).Send()
return nil, err
}
}
archivers := []map[string]interface{}{
{
"enabled": true,
@@ -281,6 +297,7 @@ func FrontendConfigFromStruct(cfg *config.Config) (map[string]interface{}, error
"min_uppercase_characters": cfg.PasswordPolicy.MinUpperCaseCharacters,
"min_digits": cfg.PasswordPolicy.MinDigits,
"min_special_characters": cfg.PasswordPolicy.MinSpecialCharacters,
"banned_passwords_list": bannedPasswordsList,
},
"notifications": map[string]interface{}{
"endpoints": []string{"list", "get", "delete"},
@@ -301,3 +318,31 @@ func FrontendConfigFromStruct(cfg *config.Config) (map[string]interface{}, error
},
}, nil
}
func readMultilineFile(path string) (map[string]struct{}, error) {
if !fileExists(path) {
path = filepath.Join(defaults.BaseConfigPath(), path)
}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
data := make(map[string]struct{})
for scanner.Scan() {
line := scanner.Text()
if line != "" {
data[line] = struct{}{}
}
}
return data, err
}
func fileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir()
}

View File

@@ -109,7 +109,14 @@ Feature: enforce password on public link
| password | Pas1 |
Then the HTTP status code should be "<http-code>"
And the OCS status code should be "400"
And the OCS status message should be "password validation failed"
And the OCS status message should be:
"""
at least 13 characters are required
at least 3 lowercase letters are required
at least 2 uppercase letters are required
at least 2 numbers are required
at least 2 special characters are required !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
"""
Examples:
| ocs-api-version | http-code |
| 1 | 200 |
@@ -168,11 +175,11 @@ Feature: enforce password on public link
And the OCS status code should be "400"
And the OCS status message should be:
"""
missing required password at least 13 characters are required
at least 13 characters are required
at least 3 lowercase letters are required
at least 2 uppercase letters are required
at least 1 numbers are required
at least 2 special characters are required. !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
at least 2 special characters are required !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
"""
Examples:
| ocs-api-version | http-code |
@@ -231,7 +238,7 @@ Feature: enforce password on public link
And the OCS status message should be "<message>"
Examples:
| config | config-value | password | message |
| FRONTEND_PASSWORD_POLICY_MIN_CHARACTERS | 5 | 1234 | missing required password at least 5 characters are required |
| FRONTEND_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS | 3 | TesT | missing required password at least 3 lowercase letters are required |
| FRONTEND_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS | 3 | TesT | missing required password at least 3 uppercase letters are required |
| FRONTEND_PASSWORD_POLICY_MIN_DIGITS | 2 | test1 | missing required password at least 2 numbers are required |
| FRONTEND_PASSWORD_POLICY_MIN_CHARACTERS | 5 | 1234 | at least 5 characters are required |
| FRONTEND_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS | 3 | TesT | at least 3 lowercase letters are required |
| FRONTEND_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS | 3 | TesT | at least 3 uppercase letters are required |
| FRONTEND_PASSWORD_POLICY_MIN_DIGITS | 2 | test1 | at least 2 numbers are required |

View File

@@ -172,32 +172,35 @@ func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Requ
w.WriteHeader(http.StatusInternalServerError)
return
}
if tfRes.Status.Code != rpc.Code_CODE_OK {
if tfRes.Status.Code == rpc.Code_CODE_OK {
sRes, err := client.Stat(ctx, &provider.StatRequest{
Ref: ref,
})
if err != nil {
log.Error().Err(err).Msg("error sending grpc touch file request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if sRes.Status.Code != rpc.Code_CODE_OK {
log.Error().Interface("status", sRes.Status).Msg("error touching file")
errors.HandleErrorStatus(&log, w, sRes.Status)
return
}
w.Header().Set(net.HeaderETag, sRes.Info.Etag)
w.Header().Set(net.HeaderOCETag, sRes.Info.Etag)
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(*sRes.Info.Id))
w.Header().Set(net.HeaderLastModified, net.RFC1123Z(sRes.Info.Mtime))
w.WriteHeader(http.StatusCreated)
return
}
if tfRes.Status.Code != rpc.Code_CODE_ALREADY_EXISTS {
log.Error().Interface("status", tfRes.Status).Msg("error touching file")
w.WriteHeader(http.StatusInternalServerError)
errors.HandleErrorStatus(&log, w, tfRes.Status)
return
}
sRes, err := client.Stat(ctx, &provider.StatRequest{
Ref: ref,
})
if err != nil {
log.Error().Err(err).Msg("error sending grpc touch file request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if sRes.Status.Code != rpc.Code_CODE_OK {
log.Error().Interface("status", sRes.Status).Msg("error touching file")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set(net.HeaderETag, sRes.Info.Etag)
w.Header().Set(net.HeaderOCETag, sRes.Info.Etag)
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(*sRes.Info.Id))
w.Header().Set(net.HeaderLastModified, net.RFC1123Z(sRes.Info.Mtime))
w.WriteHeader(http.StatusCreated)
return
}
utils.AppendPlainToOpaque(opaque, net.HeaderUploadLength, strconv.FormatInt(length, 10))

View File

@@ -88,12 +88,13 @@ type CapabilitiesGraph struct {
// CapabilitiesPasswordPolicy hold the password policy capabilities
type CapabilitiesPasswordPolicy struct {
MinCharacters int `json:"min_characters" xml:"min_characters" mapstructure:"min_characters"`
MaxCharacters int `json:"max_characters" xml:"max_characters" mapstructure:"max_characters"`
MinLowerCaseCharacters int `json:"min_lowercase_characters" xml:"min_lowercase_characters" mapstructure:"min_lowercase_characters"`
MinUpperCaseCharacters int `json:"min_uppercase_characters" xml:"min_uppercase_characters" mapstructure:"min_uppercase_characters"`
MinDigits int `json:"min_digits" xml:"min_digits" mapstructure:"min_digits"`
MinSpecialCharacters int `json:"min_special_characters" xml:"min_special_characters" mapstructure:"min_special_characters"`
MinCharacters int `json:"min_characters" xml:"min_characters" mapstructure:"min_characters"`
MaxCharacters int `json:"max_characters" xml:"max_characters" mapstructure:"max_characters"`
MinLowerCaseCharacters int `json:"min_lowercase_characters" xml:"min_lowercase_characters" mapstructure:"min_lowercase_characters"`
MinUpperCaseCharacters int `json:"min_uppercase_characters" xml:"min_uppercase_characters" mapstructure:"min_uppercase_characters"`
MinDigits int `json:"min_digits" xml:"min_digits" mapstructure:"min_digits"`
MinSpecialCharacters int `json:"min_special_characters" xml:"min_special_characters" mapstructure:"min_special_characters"`
BannedPasswordsList map[string]struct{} `json:"-" xml:"-" mapstructure:"banned_passwords_list"`
}
// CapabilitiesGraphUsers holds the graph user capabilities

View File

@@ -153,7 +153,7 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request,
if err := h.passwordValidator.Validate(password); err != nil {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: "password validation failed",
Message: err.Error(),
Error: fmt.Errorf("password validation failed: %w", err),
}
}
@@ -479,7 +479,7 @@ func (h *Handler) updatePublicShare(w http.ResponseWriter, r *http.Request, shar
// skip validation if the clear password scenario
if len(newPassword[0]) > 0 {
if err := h.passwordValidator.Validate(newPassword[0]); err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, fmt.Errorf("missing required password %w", err).Error(), err)
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, err.Error(), err)
return
}
}

View File

@@ -1592,7 +1592,7 @@ func publicPwdEnforced(c *config.Config) passwordEnforced {
func passwordPolicies(c *config.Config) password.Validator {
if c.Capabilities.Capabilities == nil || c.Capabilities.Capabilities.PasswordPolicy == nil {
return password.NewPasswordPolicy(0, 0, 0, 0, 0)
return password.NewPasswordPolicy(0, 0, 0, 0, 0, nil)
}
return password.NewPasswordPolicy(
c.Capabilities.Capabilities.PasswordPolicy.MinCharacters,
@@ -1600,6 +1600,7 @@ func passwordPolicies(c *config.Config) password.Validator {
c.Capabilities.Capabilities.PasswordPolicy.MinUpperCaseCharacters,
c.Capabilities.Capabilities.PasswordPolicy.MinDigits,
c.Capabilities.Capabilities.PasswordPolicy.MinSpecialCharacters,
c.Capabilities.Capabilities.PasswordPolicy.BannedPasswordsList,
)
}

View File

@@ -23,18 +23,20 @@ type Policies struct {
minUpperCaseCharacters int
minDigits int
minSpecialCharacters int
bannedPasswordsList map[string]struct{}
digitsRegexp *regexp.Regexp
specialCharactersRegexp *regexp.Regexp
}
// NewPasswordPolicy returns a new NewPasswordPolicy instance
func NewPasswordPolicy(minCharacters, minLowerCaseCharacters, minUpperCaseCharacters, minDigits, minSpecialCharacters int) Validator {
func NewPasswordPolicy(minCharacters, minLowerCaseCharacters, minUpperCaseCharacters, minDigits, minSpecialCharacters int, bannedPasswordsList map[string]struct{}) Validator {
p := &Policies{
minCharacters: minCharacters,
minLowerCaseCharacters: minLowerCaseCharacters,
minUpperCaseCharacters: minUpperCaseCharacters,
minDigits: minDigits,
minSpecialCharacters: minSpecialCharacters,
bannedPasswordsList: bannedPasswordsList,
}
p.digitsRegexp = regexp.MustCompile("[0-9]")
@@ -48,7 +50,11 @@ func (s Policies) Validate(str string) error {
if !utf8.ValidString(str) {
return fmt.Errorf("the password contains invalid characters")
}
err := s.validateCharacters(str)
err := s.validateBannedList(str)
if err != nil {
return err
}
err = s.validateCharacters(str)
if err != nil {
allErr = errors.Join(allErr, err)
}
@@ -74,6 +80,16 @@ func (s Policies) Validate(str string) error {
return nil
}
func (s Policies) validateBannedList(str string) error {
if len(s.bannedPasswordsList) == 0 {
return nil
}
if _, ok := s.bannedPasswordsList[str]; ok {
return fmt.Errorf("unfortunately, your password is commonly used. please pick a harder-to-guess password for your safety")
}
return nil
}
func (s Policies) validateCharacters(str string) error {
if s.count(str) < s.minCharacters {
return fmt.Errorf("at least %d characters are required", s.minCharacters)
@@ -104,7 +120,7 @@ func (s Policies) validateDigits(str string) error {
func (s Policies) validateSpecialCharacters(str string) error {
if s.countSpecialCharacters(str) < s.minSpecialCharacters {
return fmt.Errorf("at least %d special characters are required. %s", s.minSpecialCharacters, _defaultSpecialCharacters)
return fmt.Errorf("at least %d special characters are required %s", s.minSpecialCharacters, _defaultSpecialCharacters)
}
return nil
}

2
vendor/modules.txt vendored
View File

@@ -356,7 +356,7 @@ github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1
github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1
github.com/cs3org/go-cs3apis/cs3/tx/v1beta1
github.com/cs3org/go-cs3apis/cs3/types/v1beta1
# github.com/cs3org/reva/v2 v2.16.1-0.20230915081009-843fe781fbf8
# github.com/cs3org/reva/v2 v2.16.1-0.20230921092447-6caa589a0ae8
## explicit; go 1.20
github.com/cs3org/reva/v2/cmd/revad/internal/grace
github.com/cs3org/reva/v2/cmd/revad/runtime