Skip to content

Commit

Permalink
Add the password policies
Browse files Browse the repository at this point in the history
  • Loading branch information
2403905 committed Sep 1, 2023
1 parent bf1bb01 commit 15d55b1
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 15 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/add-passwod-policies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Add the password policies

Add the password policies OCIS-3767

https://github.com/cs3org/reva/pull/4147
25 changes: 18 additions & 7 deletions internal/http/services/owncloud/ocs/data/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@ type CapabilitiesData struct {

// Capabilities groups several capability aspects
type Capabilities struct {
Core *CapabilitiesCore `json:"core" xml:"core"`
Checksums *CapabilitiesChecksums `json:"checksums" xml:"checksums"`
Files *CapabilitiesFiles `json:"files" xml:"files" mapstructure:"files"`
Dav *CapabilitiesDav `json:"dav" xml:"dav"`
FilesSharing *CapabilitiesFilesSharing `json:"files_sharing" xml:"files_sharing" mapstructure:"files_sharing"`
Spaces *Spaces `json:"spaces,omitempty" xml:"spaces,omitempty" mapstructure:"spaces"`
Graph *CapabilitiesGraph `json:"graph,omitempty" xml:"graph,omitempty" mapstructure:"graph"`
Core *CapabilitiesCore `json:"core" xml:"core"`
Checksums *CapabilitiesChecksums `json:"checksums" xml:"checksums"`
Files *CapabilitiesFiles `json:"files" xml:"files" mapstructure:"files"`
Dav *CapabilitiesDav `json:"dav" xml:"dav"`
FilesSharing *CapabilitiesFilesSharing `json:"files_sharing" xml:"files_sharing" mapstructure:"files_sharing"`
Spaces *Spaces `json:"spaces,omitempty" xml:"spaces,omitempty" mapstructure:"spaces"`
Graph *CapabilitiesGraph `json:"graph,omitempty" xml:"graph,omitempty" mapstructure:"graph"`
PasswordPolicies *CapabilitiesPasswordPolicies `json:"password_policies,omitempty" xml:"password_policies,omitempty" mapstructure:"password_policies"`

Notifications *CapabilitiesNotifications `json:"notifications,omitempty" xml:"notifications,omitempty"`
}
Expand Down Expand Up @@ -85,6 +86,16 @@ type CapabilitiesGraph struct {
Users CapabilitiesGraphUsers `json:"users" xml:"users" mapstructure:"users"`
}

// CapabilitiesPasswordPolicies hold the password policies capabilities
type CapabilitiesPasswordPolicies struct {
MinCharacters int `json:"min_characters" xml:"min_characters" mapstructure:"min_characters"`
MinLowerCaseCharacters int `json:"min_lower_case_characters" xml:"min_lower_case_characters" mapstructure:"min_lower_case_characters"`
MinUpperCaseCharacters int `json:"min_upper_case_characters" xml:"min_upper_case_characters" mapstructure:"min_upper_case_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"`
AllowedSpecialCharacters string `json:"allowed_special_characters" xml:"allowed_special_characters" mapstructure:"allowed_special_characters"`
}

// CapabilitiesGraphUsers holds the graph user capabilities
type CapabilitiesGraphUsers struct {
ReadOnlyAttributes []string `json:"read_only_attributes" xml:"read_only_attributes" mapstructure:"read_only_attributes"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"fmt"
"net/http"
"strconv"
"strings"

permissionsv1beta1 "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
Expand Down Expand Up @@ -142,14 +141,21 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request,
}
}

password := strings.TrimSpace(r.FormValue("password"))
password := r.FormValue("password")
if h.enforcePassword(permKey) && len(password) == 0 {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: "missing required password",
Error: errors.New("missing required password"),
}
}
if err := h.passwordValidator.Validate(password); len(password) > 0 && err != nil {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: "password validation failed",
Error: fmt.Errorf("password validation failed: %w", err),
}
}

if statInfo != nil && statInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE {
// Single file shares should never have delete or create permissions
Expand Down Expand Up @@ -460,12 +466,18 @@ func (h *Handler) updatePublicShare(w http.ResponseWriter, r *http.Request, shar
newPassword, ok := r.Form["password"]
// enforcePassword
if h.enforcePassword(permKey) {
if (!ok && !share.PasswordProtected) || (ok && len(strings.TrimSpace(newPassword[0])) == 0) {
if !ok && !share.PasswordProtected || ok && len(newPassword[0]) == 0 {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing required password", err)
return
}
}

// skip validation if the clear password scenario
if err := h.passwordValidator.Validate(newPassword[0]); ok && err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, fmt.Errorf("missing required password %w", err).Error(), err)
return
}

// update or clear password
if ok {
updatesFound = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"mime"
"net/http"
"path"
Expand All @@ -39,6 +40,7 @@ import (
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/password"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
"google.golang.org/grpc/metadata"
Expand Down Expand Up @@ -87,6 +89,7 @@ type Handler struct {
deniable bool
resharing bool
publicPasswordEnforced passwordEnforced
passwordValidator password.Validator

getClient GatewayClientGetter
}
Expand Down Expand Up @@ -122,7 +125,8 @@ func getCacheWarmupManager(c *config.Config) (sharecache.Warmup, error) {
type GatewayClientGetter func() (gateway.GatewayAPIClient, error)

// Init initializes this and any contained handlers
func (h *Handler) Init(c *config.Config) {
func (h *Handler) Init(c *config.Config) error {
var err error
h.gatewayAddr = c.GatewaySvc
h.machineAuthAPIKey = c.MachineAuthAPIKey
h.storageRegistryAddr = c.StorageregistrySvc
Expand All @@ -138,20 +142,29 @@ func (h *Handler) Init(c *config.Config) {
h.deniable = c.EnableDenials
h.resharing = resharing(c)
h.publicPasswordEnforced = publicPwdEnforced(c)
h.passwordValidator, err = passwordPolicies(c)
if err != nil {
return err
}

h.statCache = cache.GetStatCache(c.StatCacheStore, c.StatCacheNodes, c.StatCacheDatabase, "stat", time.Duration(c.StatCacheTTL)*time.Second, c.StatCacheSize)
if c.CacheWarmupDriver != "" {
cwm, err := getCacheWarmupManager(c)
if err == nil {
go h.startCacheWarmup(cwm)
if err != nil {
return err
}
go h.startCacheWarmup(cwm)
}
h.getClient = h.getPoolClient
return nil
}

// InitWithGetter initializes the handler and adds the clientGetter
func (h *Handler) InitWithGetter(c *config.Config, clientGetter GatewayClientGetter) {
h.Init(c)
err := h.Init(c)
if err != nil {
log.Fatal(err)
}
h.getClient = clientGetter
}

Expand Down Expand Up @@ -1581,6 +1594,30 @@ func publicPwdEnforced(c *config.Config) passwordEnforced {
return enf
}

func passwordPolicies(c *config.Config) (password.Validator, error) {
var pv password.Validator
var err error
if c.Capabilities.Capabilities == nil || c.Capabilities.Capabilities.PasswordPolicies == nil {
pv, err = password.NewDefaultPasswordPolicies()
if err != nil {
return nil, fmt.Errorf("can't init the Password Policies %w", err)
}
return pv, nil
}
pv, err = password.NewPasswordPolicies(
c.Capabilities.Capabilities.PasswordPolicies.MinCharacters,
c.Capabilities.Capabilities.PasswordPolicies.MinLowerCaseCharacters,
c.Capabilities.Capabilities.PasswordPolicies.MinUpperCaseCharacters,
c.Capabilities.Capabilities.PasswordPolicies.MinDigits,
c.Capabilities.Capabilities.PasswordPolicies.MinSpecialCharacters,
c.Capabilities.Capabilities.PasswordPolicies.AllowedSpecialCharacters,
)
if err != nil {
return nil, fmt.Errorf("can't init the Password Policies %w", err)
}
return pv, nil
}

// sufficientPermissions returns true if the `existing` permissions contain the `requested` permissions
func sufficientPermissions(existing, requested *provider.ResourcePermissions, islink bool) bool {
ep := conversions.RoleFromResourcePermissions(existing, islink).OCSPermissions()
Expand Down
5 changes: 4 additions & 1 deletion internal/http/services/owncloud/ocs/ocs.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ func (s *svc) routerInit(log *zerolog.Logger) error {
capabilitiesHandler.Init(s.c)
usersHandler.Init(s.c)
configHandler.Init(s.c)
sharesHandler.Init(s.c)
err := sharesHandler.Init(s.c)
if err != nil {
log.Fatal().Msg(err.Error())
}
shareesHandler.Init(s.c)

s.router.Route("/v{version:(1|2)}.php", func(r chi.Router) {
Expand Down
167 changes: 167 additions & 0 deletions pkg/password/password_policies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package password

import (
"errors"
"fmt"
"regexp"
"strings"
"unicode/utf8"
)

var defaultSpecialCharacters = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"

type Validator interface {
Validate(str string) error
}

type Policies struct {
minCharacters int
minLowerCaseCharacters int
minUpperCaseCharacters int
minDigits int
minSpecialCharacters int
allowedSpecialCharacters string
digits *regexp.Regexp
specialCharacters *regexp.Regexp
}

func NewPasswordPolicies(minCharacters, minLowerCaseCharacters, minUpperCaseCharacters, minDigits, minSpecialCharacters int,
allowedSpecialCharacters string) (Validator, error) {

digits := regexp.MustCompile("[0-9]")
if len(allowedSpecialCharacters) > 0 {
defaultSpecialCharacters = allowedSpecialCharacters
}
specialCharacters, err := regexp.Compile("[" + regexp.QuoteMeta(defaultSpecialCharacters) + "]")
if err != nil {
return nil, err
}
return &Policies{
minCharacters: minCharacters,
minLowerCaseCharacters: minLowerCaseCharacters,
minUpperCaseCharacters: minUpperCaseCharacters,
minDigits: minDigits,
minSpecialCharacters: minSpecialCharacters,
allowedSpecialCharacters: allowedSpecialCharacters,
digits: digits,
specialCharacters: specialCharacters,
}, nil
}

func NewDefaultPasswordPolicies() (Validator, error) {
return NewPasswordPolicies(0, 0, 0, 0, 0, "")
}

func (s Policies) Validate(str string) error {
var allErr error
if !utf8.ValidString(str) {
return fmt.Errorf("the password contains invalid characters")
}
err := s.validateCharacters(str)
if err != nil {
allErr = errors.Join(allErr, err)
}
err = s.validateLowerCase(str)
if err != nil {
allErr = errors.Join(allErr, err)
}
err = s.validateUpperCase(str)
if err != nil {
allErr = errors.Join(allErr, err)
}
err = s.validateDigits(str)
if err != nil {
allErr = errors.Join(allErr, err)
}
err = s.validateSpecialCharacters(str)
if err != nil {
allErr = errors.Join(allErr, err)
}
if allErr != nil {
return allErr
}
return nil
}

func (s Policies) validateCharacters(str string) error {
if s.count(str) < s.minCharacters {
if s.minCharacters == 1 {
return fmt.Errorf("at least one character is required")
}
return fmt.Errorf("at least %d characters are required", s.minCharacters)
}
return nil
}

func (s Policies) validateLowerCase(str string) error {
if s.countLowerCaseCharacters(str) < s.minLowerCaseCharacters {
if s.minLowerCaseCharacters == 1 {
return fmt.Errorf("at least one lowercase letter is required")
}
return fmt.Errorf("at least %d lowercase letters are required", s.minLowerCaseCharacters)
}
return nil
}

func (s Policies) validateUpperCase(str string) error {
if s.countUpperCaseCharacters(str) < s.minUpperCaseCharacters {
if s.minUpperCaseCharacters == 1 {
return fmt.Errorf("at least one uppercase letter is required")
}
return fmt.Errorf("at least %d uppercase letters are required", s.minUpperCaseCharacters)
}
return nil
}

func (s Policies) validateDigits(str string) error {
if s.countDigits(str) < s.minDigits {
if s.minDigits == 1 {
return fmt.Errorf("at least one number is required")
}
return fmt.Errorf("at least %d numbers are required", s.minDigits)
}
return nil
}

func (s Policies) validateSpecialCharacters(str string) error {
if s.countSpecialCharacters(str) < s.minSpecialCharacters {
if s.minSpecialCharacters == 1 {
return fmt.Errorf("at least one special character is required. %s", s.allowedSpecialCharacters)
}
return fmt.Errorf("at least %d special characters are required. %s", s.minSpecialCharacters, s.allowedSpecialCharacters)
}
return nil
}

func (s Policies) count(str string) int {
return utf8.RuneCount([]byte(str))
}

func (s Policies) countLowerCaseCharacters(str string) int {
var count int
for _, c := range str {
if strings.ToLower(string(c)) == string(c) && strings.ToUpper(string(c)) != string(c) {
count++
}
}
return count
}

func (s Policies) countUpperCaseCharacters(str string) int {
var count int
for _, c := range str {
if strings.ToUpper(string(c)) == string(c) && strings.ToLower(string(c)) != string(c) {
count++
}
}
return count
}

func (s Policies) countDigits(str string) int {
return len(s.digits.FindAllStringIndex(str, -1))
}

func (s Policies) countSpecialCharacters(str string) int {
res := s.specialCharacters.FindAllStringIndex(str, -1)
return len(res)
}

0 comments on commit 15d55b1

Please sign in to comment.