Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the password policies #4147

Merged
merged 2 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Dockerfile.revad-ceph
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ RUN dnf update --exclude=ceph-iscsi -y && dnf install -y \
librbd-devel \
librados-devel

ADD https://golang.org/dl/go1.19.linux-amd64.tar.gz \
go1.19.linux-amd64.tar.gz
ADD https://golang.org/dl/go1.20.linux-amd64.tar.gz \
go1.20.linux-amd64.tar.gz

RUN rm -rf /usr/local/go && \
tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz && \
rm go1.19.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.20.linux-amd64.tar.gz && \
rm go1.20.linux-amd64.tar.gz

ENV PATH /go/bin:/usr/local/go/bin:$PATH
ENV GOPATH /go
Expand Down
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
26 changes: 19 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,17 @@ 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"`
MaxCharacters int `json:"max_characters" xml:"max_characters" mapstructure:"max_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"`
SpecialCharacters string `json:"special_characters" xml:"special_characters" mapstructure:"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,23 @@ 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 len(password) > 0 {
if err := h.passwordValidator.Validate(password); 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,14 +468,21 @@ 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
}
}

// update or clear password
if ok {
// 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)
return
}
}
updatesFound = true
logger.Info().Str("shares", "update").Msg("password updated")
updates = append(updates, &link.UpdatePublicShareRequest_Update{
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.NewPasswordPolicies(0, 0, 0, 0, 0, "")
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.SpecialCharacters,
)
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
157 changes: 157 additions & 0 deletions pkg/password/password_policies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package password

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

// Validator describes the interface providing a password Validate method
type Validator interface {
2403905 marked this conversation as resolved.
Show resolved Hide resolved
Validate(str string) error
}

// Policies represents a password validation rules
type Policies struct {
2403905 marked this conversation as resolved.
Show resolved Hide resolved
minCharacters int
minLowerCaseCharacters int
minUpperCaseCharacters int
minDigits int
minSpecialCharacters int
specialCharacters string
digitsRegexp *regexp.Regexp
specialCharactersRegexp *regexp.Regexp
}

// NewPasswordPolicies returns a new NewPasswordPolicies instance
func NewPasswordPolicies(minCharacters, minLowerCaseCharacters, minUpperCaseCharacters, minDigits, minSpecialCharacters int,
2403905 marked this conversation as resolved.
Show resolved Hide resolved
specialCharacters string) (Validator, error) {
p := &Policies{
minCharacters: minCharacters,
minLowerCaseCharacters: minLowerCaseCharacters,
minUpperCaseCharacters: minUpperCaseCharacters,
minDigits: minDigits,
minSpecialCharacters: minSpecialCharacters,
specialCharacters: specialCharacters,
}

p.digitsRegexp = regexp.MustCompile("[0-9]")
if len(specialCharacters) > 0 {
var err error
p.specialCharactersRegexp, err = regexp.Compile(specialCharactersExp(specialCharacters))
if err != nil {
return nil, err
}
}
return p, nil
}

// Validate implements a password validation regarding the policy
func (s Policies) Validate(str string) error {
2403905 marked this conversation as resolved.
Show resolved Hide resolved
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 {
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 {
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 {
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 {
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 {
return fmt.Errorf("at least %d special characters are required. %s", s.minSpecialCharacters, s.specialCharacters)
}
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.digitsRegexp.FindAllStringIndex(str, -1))
}

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

func specialCharactersExp(str string) string {
// escape the '-' character because it is a not meta-characters but, they are special inside of []
return "[" + strings.ReplaceAll(regexp.QuoteMeta(str), "-", `\-`) + "]"
}
Loading
Loading