[full-ci] Ocm tus (#7998)

* Add service account configuration for the ocm service

* Bump reva

* Add changelog
This commit is contained in:
Andre Duffeck
2023-12-18 10:05:31 +01:00
committed by GitHub
parent 96c0e71b93
commit 5749348b11
16 changed files with 530 additions and 80 deletions
@@ -515,6 +515,14 @@ func (s *service) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateRec
func (s *service) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
if user.Id.GetType() == userpb.UserType_USER_TYPE_SERVICE {
var uid userpb.UserId
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &uid)
user = &userpb.User{
Id: &uid,
}
}
ocmshare, err := s.repo.GetReceivedShare(ctx, user, req.Ref)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
@@ -523,7 +531,7 @@ func (s *service) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedO
}, nil
}
return &ocm.GetReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error getting received share"),
Status: status.NewInternal(ctx, "error getting received share: "+err.Error()),
}, nil
}
@@ -68,6 +68,7 @@ type config struct {
}
type passwordPolicy struct {
Disabled bool `mapstructure:"disabled"`
MinCharacters int `mapstructure:"min_characters"`
MinLowerCaseCharacters int `mapstructure:"min_lowercase_characters"`
MinUpperCaseCharacters int `mapstructure:"min_uppercase_characters"`
@@ -173,9 +174,10 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) {
func newPasswordPolicy(c *passwordPolicy) password.Validator {
if c == nil {
return password.NewPasswordPolicy(0, 0, 0, 0, 0, nil)
return password.NewPasswordPolicy(true, 0, 0, 0, 0, 0, nil)
}
return password.NewPasswordPolicy(
c.Disabled,
c.MinCharacters,
c.MinLowerCaseCharacters,
c.MinUpperCaseCharacters,
@@ -137,6 +137,7 @@ func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Requ
length, err := getContentLength(w, r)
if err != nil {
log.Error().Err(err).Msg("error getting the content length")
w.WriteHeader(http.StatusBadRequest)
return
}
@@ -117,6 +117,7 @@ type CapabilitiesGraph struct {
// CapabilitiesPasswordPolicy hold the password policy capabilities
type CapabilitiesPasswordPolicy struct {
Disabled bool `json:"disabled" xml:"disabled" mapstructure:"disabled"`
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"`
@@ -1734,9 +1734,10 @@ 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, nil)
return password.NewPasswordPolicy(true, 0, 0, 0, 0, 0, nil)
}
return password.NewPasswordPolicy(
c.Capabilities.Capabilities.PasswordPolicy.Disabled,
c.Capabilities.Capabilities.PasswordPolicy.MinCharacters,
c.Capabilities.Capabilities.PasswordPolicy.MinLowerCaseCharacters,
c.Capabilities.Capabilities.PasswordPolicy.MinUpperCaseCharacters,
+30 -43
View File
@@ -30,6 +30,7 @@ import (
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
@@ -59,8 +60,11 @@ type driver struct {
}
type config struct {
GatewaySVC string `mapstructure:"gatewaysvc"`
Insecure bool `mapstructure:"insecure"`
GatewaySVC string `mapstructure:"gatewaysvc"`
Insecure bool `mapstructure:"insecure"`
StorageRoot string `mapstructure:"storage_root"`
ServiceAccountID string `mapstructure:"service_account_id"`
ServiceAccountSecret string `mapstructure:"service_account_secret"`
}
func (c *config) ApplyDefaults() {
@@ -136,15 +140,19 @@ func shareInfoFromReference(ref *provider.Reference) (*ocmpb.ShareId, string) {
}
func (d *driver) getWebDAVFromShare(ctx context.Context, shareID *ocmpb.ShareId) (*ocmpb.ReceivedShare, string, string, error) {
func (d *driver) getWebDAVFromShare(ctx context.Context, forUser *userpb.UserId, shareID *ocmpb.ShareId) (*ocmpb.ReceivedShare, string, string, error) {
// TODO: we may want to cache the share
res, err := d.gateway.GetReceivedOCMShare(ctx, &ocmpb.GetReceivedOCMShareRequest{
req := &ocmpb.GetReceivedOCMShareRequest{
Ref: &ocmpb.ShareReference{
Spec: &ocmpb.ShareReference_Id{
Id: shareID,
},
},
})
}
if forUser != nil {
req.Opaque = utils.AppendJSONToOpaque(nil, "userid", forUser)
}
res, err := d.gateway.GetReceivedOCMShare(ctx, req)
if err != nil {
return nil, "", "", err
}
@@ -173,10 +181,10 @@ func getWebDAVProtocol(protocols []*ocmpb.Protocol) (*ocmpb.WebDAVProtocol, bool
return nil, false
}
func (d *driver) webdavClient(ctx context.Context, ref *provider.Reference) (*gowebdav.Client, *ocmpb.ReceivedShare, string, error) {
func (d *driver) webdavClient(ctx context.Context, forUser *userpb.UserId, ref *provider.Reference) (*gowebdav.Client, *ocmpb.ReceivedShare, string, error) {
id, rel := shareInfoFromReference(ref)
share, endpoint, secret, err := d.getWebDAVFromShare(ctx, id)
share, endpoint, secret, err := d.getWebDAVFromShare(ctx, forUser, id)
if err != nil {
return nil, nil, "", err
}
@@ -199,7 +207,7 @@ func (d *driver) webdavClient(ctx context.Context, ref *provider.Reference) (*go
}
func (d *driver) CreateDir(ctx context.Context, ref *provider.Reference) error {
client, _, rel, err := d.webdavClient(ctx, ref)
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return err
}
@@ -207,7 +215,7 @@ func (d *driver) CreateDir(ctx context.Context, ref *provider.Reference) error {
}
func (d *driver) Delete(ctx context.Context, ref *provider.Reference) error {
client, _, rel, err := d.webdavClient(ctx, ref)
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return err
}
@@ -215,7 +223,7 @@ func (d *driver) Delete(ctx context.Context, ref *provider.Reference) error {
}
func (d *driver) TouchFile(ctx context.Context, ref *provider.Reference, markprocessing bool, mtime string) error {
client, _, rel, err := d.webdavClient(ctx, ref)
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return err
}
@@ -223,7 +231,7 @@ func (d *driver) TouchFile(ctx context.Context, ref *provider.Reference, markpro
}
func (d *driver) Move(ctx context.Context, oldRef, newRef *provider.Reference) error {
client, _, relOld, err := d.webdavClient(ctx, oldRef)
client, _, relOld, err := d.webdavClient(ctx, nil, oldRef)
if err != nil {
return err
}
@@ -263,7 +271,7 @@ func convertStatToResourceInfo(ref *provider.Reference, f fs.FileInfo, share *oc
}
webdavProtocol, _ := getWebDAVProtocol(share.Protocols)
return &provider.ResourceInfo{
ri := provider.ResourceInfo{
Type: t,
Id: id,
MimeType: mime.Detect(f.IsDir(), f.Name()),
@@ -278,11 +286,17 @@ func convertStatToResourceInfo(ref *provider.Reference, f fs.FileInfo, share *oc
Checksum: &provider.ResourceChecksum{
Type: provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_INVALID,
},
}, nil
}
if f.(gowebdav.File).StatusCode() == 425 {
ri.Opaque = utils.AppendPlainToOpaque(ri.Opaque, "status", "processing")
}
return &ri, nil
}
func (d *driver) GetMD(ctx context.Context, ref *provider.Reference, _ []string, _ []string) (*provider.ResourceInfo, error) {
client, share, rel, err := d.webdavClient(ctx, ref)
client, share, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return nil, err
}
@@ -299,7 +313,7 @@ func (d *driver) GetMD(ctx context.Context, ref *provider.Reference, _ []string,
}
func (d *driver) ListFolder(ctx context.Context, ref *provider.Reference, _ []string, _ []string) ([]*provider.ResourceInfo, error) {
client, share, rel, err := d.webdavClient(ctx, ref)
client, share, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return nil, err
}
@@ -320,35 +334,8 @@ func (d *driver) ListFolder(ctx context.Context, ref *provider.Reference, _ []st
return res, nil
}
func (d *driver) InitiateUpload(ctx context.Context, ref *provider.Reference, _ int64, _ map[string]string) (map[string]string, error) {
shareID, rel := shareInfoFromReference(ref)
p := getPathFromShareIDAndRelPath(shareID, rel)
return map[string]string{
"simple": p,
}, nil
}
func (d *driver) Upload(ctx context.Context, req storage.UploadRequest, _ storage.UploadFinishedFunc) (provider.ResourceInfo, error) {
client, _, rel, err := d.webdavClient(ctx, req.Ref)
if err != nil {
return provider.ResourceInfo{}, err
}
client.SetInterceptor(func(method string, rq *http.Request) {
// Set the content length on the request struct directly instead of the header.
// The content-length header gets reset by the golang http library before
// sendind out the request, resulting in chunked encoding to be used which
// breaks the quota checks in ocdav.
if method == "PUT" {
rq.ContentLength = req.Length
}
})
return provider.ResourceInfo{}, client.WriteStream(rel, req.Body, 0)
}
func (d *driver) Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error) {
client, _, rel, err := d.webdavClient(ctx, ref)
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return nil, err
}
+405
View File
@@ -0,0 +1,405 @@
// Copyright 2018-2023 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocm
import (
"context"
"crypto/md5"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"hash"
"hash/adler32"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
tusd "github.com/tus/tusd/pkg/handler"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/storage"
"github.com/cs3org/reva/v2/pkg/utils"
)
var defaultFilePerm = os.FileMode(0664)
func (d *driver) ListUploadSessions(ctx context.Context, filter storage.UploadSessionFilter) ([]storage.UploadSession, error) {
return []storage.UploadSession{}, nil
}
func (d *driver) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error) {
shareID, rel := shareInfoFromReference(ref)
p := getPathFromShareIDAndRelPath(shareID, rel)
info := tusd.FileInfo{
MetaData: tusd.MetaData{
"filename": filepath.Base(p),
"dir": filepath.Dir(p),
},
Size: uploadLength,
}
upload, err := d.NewUpload(ctx, info)
if err != nil {
return nil, err
}
info, _ = upload.GetInfo(ctx)
return map[string]string{
"simple": info.ID,
"tus": info.ID,
}, nil
}
func (d *driver) Upload(ctx context.Context, req storage.UploadRequest, _ storage.UploadFinishedFunc) (provider.ResourceInfo, error) {
shareID, _ := shareInfoFromReference(req.Ref)
u, err := d.GetUpload(ctx, shareID.OpaqueId)
if err != nil {
return provider.ResourceInfo{}, err
}
info, err := u.GetInfo(ctx)
if err != nil {
return provider.ResourceInfo{}, err
}
client, _, rel, err := d.webdavClient(ctx, nil, &provider.Reference{
Path: filepath.Join(info.MetaData["dir"], info.MetaData["filename"]),
})
if err != nil {
return provider.ResourceInfo{}, err
}
client.SetInterceptor(func(method string, rq *http.Request) {
// Set the content length on the request struct directly instead of the header.
// The content-length header gets reset by the golang http library before
// sendind out the request, resulting in chunked encoding to be used which
// breaks the quota checks in ocdav.
if method == "PUT" {
rq.ContentLength = req.Length
}
})
return provider.ResourceInfo{}, client.WriteStream(rel, req.Body, 0)
}
// UseIn tells the tus upload middleware which extensions it supports.
func (d *driver) UseIn(composer *tusd.StoreComposer) {
composer.UseCore(d)
composer.UseTerminater(d)
composer.UseConcater(d)
composer.UseLengthDeferrer(d)
}
// AsTerminatableUpload returns a TerminatableUpload
// To implement the termination extension as specified in https://tus.io/protocols/resumable-upload.html#termination
// the storage needs to implement AsTerminatableUpload
func (d *driver) AsTerminatableUpload(up tusd.Upload) tusd.TerminatableUpload {
return up.(*upload)
}
// AsLengthDeclarableUpload returns a LengthDeclarableUpload
// To implement the creation-defer-length extension as specified in https://tus.io/protocols/resumable-upload.html#creation
// the storage needs to implement AsLengthDeclarableUpload
func (d *driver) AsLengthDeclarableUpload(up tusd.Upload) tusd.LengthDeclarableUpload {
return up.(*upload)
}
// AsConcatableUpload returns a ConcatableUpload
// To implement the concatenation extension as specified in https://tus.io/protocols/resumable-upload.html#concatenation
// the storage needs to implement AsConcatableUpload
func (d *driver) AsConcatableUpload(up tusd.Upload) tusd.ConcatableUpload {
return up.(*upload)
}
// To implement the core tus.io protocol as specified in https://tus.io/protocols/resumable-upload.html#core-protocol
// - the storage needs to implement NewUpload and GetUpload
// - the upload needs to implement the tusd.Upload interface: WriteChunk, GetInfo, GetReader and FinishUpload
// NewUpload returns a new tus Upload instance
func (d *driver) NewUpload(ctx context.Context, info tusd.FileInfo) (tusd.Upload, error) {
return NewUpload(ctx, d, d.c.StorageRoot, info)
}
// GetUpload returns the Upload for the given upload id
func (d *driver) GetUpload(ctx context.Context, id string) (tusd.Upload, error) {
return GetUpload(ctx, d, d.c.StorageRoot, id)
}
func NewUpload(ctx context.Context, d *driver, storageRoot string, info tusd.FileInfo) (tusd.Upload, error) {
if info.MetaData["filename"] == "" {
return nil, errors.New("Decomposedfs: missing filename in metadata")
}
if info.MetaData["dir"] == "" {
return nil, errors.New("Decomposedfs: missing dir in metadata")
}
uploadRoot := filepath.Join(storageRoot, "uploads")
info.ID = uuid.New().String()
user, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("no user in context")
}
info.MetaData["user"] = user.GetId().GetOpaqueId()
info.MetaData["idp"] = user.GetId().GetIdp()
info.Storage = map[string]string{
"Type": "OCM",
"Path": uploadRoot,
}
u := &upload{
Info: info,
Ctx: ctx,
d: d,
}
err := os.MkdirAll(uploadRoot, 0755)
if err != nil {
return nil, err
}
file, err := os.OpenFile(u.BinPath(), os.O_CREATE|os.O_WRONLY, defaultFilePerm)
if err != nil {
return nil, err
}
defer file.Close()
err = u.Persist()
if err != nil {
return nil, err
}
return u, nil
}
func GetUpload(ctx context.Context, d *driver, storageRoot string, id string) (tusd.Upload, error) {
info := tusd.FileInfo{}
data, err := os.ReadFile(filepath.Join(storageRoot, "uploads", id+".info"))
if err != nil {
return nil, err
}
err = json.Unmarshal(data, &info)
if err != nil {
return nil, err
}
upload := &upload{
Info: info,
Ctx: ctx,
d: d,
}
return upload, nil
}
type upload struct {
Info tusd.FileInfo
Ctx context.Context
d *driver
}
func (u *upload) InfoPath() string {
return filepath.Join(u.Info.Storage["Path"], u.Info.ID+".info")
}
func (u *upload) BinPath() string {
return filepath.Join(u.Info.Storage["Path"], u.Info.ID)
}
func (u *upload) Persist() error {
data, err := json.Marshal(u.Info)
if err != nil {
return err
}
return os.WriteFile(u.InfoPath(), data, defaultFilePerm)
}
func (u *upload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) {
file, err := os.OpenFile(u.BinPath(), os.O_WRONLY|os.O_APPEND, defaultFilePerm)
if err != nil {
return 0, err
}
defer file.Close()
// calculate cheksum here? needed for the TUS checksum extension. https://tus.io/protocols/resumable-upload.html#checksum
// TODO but how do we get the `Upload-Checksum`? WriteChunk() only has a context, offset and the reader ...
// It is sent with the PATCH request, well or in the POST when the creation-with-upload extension is used
// but the tus handler uses a context.Background() so we cannot really check the header and put it in the context ...
n, err := io.Copy(file, src)
// If the HTTP PATCH request gets interrupted in the middle (e.g. because
// the user wants to pause the upload), Go's net/http returns an io.ErrUnexpectedEOF.
// However, for the ocis driver it's not important whether the stream has ended
// on purpose or accidentally.
if err != nil && err != io.ErrUnexpectedEOF {
return n, err
}
u.Info.Offset += n
return n, u.Persist()
}
func (u *upload) GetInfo(ctx context.Context) (tusd.FileInfo, error) {
return u.Info, nil
}
func (u *upload) GetReader(ctx context.Context) (io.Reader, error) {
return os.Open(u.BinPath())
}
func (u *upload) FinishUpload(ctx context.Context) error {
log := appctx.GetLogger(u.Ctx)
// calculate the checksum of the written bytes
// they will all be written to the metadata later, so we cannot omit any of them
// TODO only calculate the checksum in sync that was requested to match, the rest could be async ... but the tests currently expect all to be present
// TODO the hashes all implement BinaryMarshaler so we could try to persist the state for resumable upload. we would neet do keep track of the copied bytes ...
sha1h := sha1.New()
md5h := md5.New()
adler32h := adler32.New()
{
f, err := os.Open(u.BinPath())
if err != nil {
// we can continue if no oc checksum header is set
log.Info().Err(err).Str("binPath", u.BinPath()).Msg("error opening binPath")
}
defer f.Close()
r1 := io.TeeReader(f, sha1h)
r2 := io.TeeReader(r1, md5h)
_, err = io.Copy(adler32h, r2)
if err != nil {
log.Info().Err(err).Msg("error copying checksums")
}
}
// compare if they match the sent checksum
// TODO the tus checksum extension would do this on every chunk, but I currently don't see an easy way to pass in the requested checksum. for now we do it in FinishUpload which is also called for chunked uploads
if u.Info.MetaData["checksum"] != "" {
var err error
parts := strings.SplitN(u.Info.MetaData["checksum"], " ", 2)
if len(parts) != 2 {
return errtypes.BadRequest("invalid checksum format. must be '[algorithm] [checksum]'")
}
switch parts[0] {
case "sha1":
err = u.checkHash(parts[1], sha1h)
case "md5":
err = u.checkHash(parts[1], md5h)
case "adler32":
err = u.checkHash(parts[1], adler32h)
default:
err = errtypes.BadRequest("unsupported checksum algorithm: " + parts[0])
}
if err != nil {
u.cleanup()
return err
}
}
// send to the remote storage via webdav
// shareID, rel := shareInfoFromReference(u.Info.MetaData["ref"])
// p := getPathFromShareIDAndRelPath(shareID, rel)
serviceUserCtx, err := utils.GetServiceUserContext(u.d.c.ServiceAccountID, u.d.gateway, u.d.c.ServiceAccountSecret)
if err != nil {
return err
}
client, _, rel, err := u.d.webdavClient(serviceUserCtx, &userpb.UserId{
OpaqueId: u.Info.MetaData["user"],
Idp: u.Info.MetaData["idp"],
}, &provider.Reference{
Path: filepath.Join(u.Info.MetaData["dir"], u.Info.MetaData["filename"]),
})
if err != nil {
u.cleanup()
return err
}
client.SetInterceptor(func(method string, rq *http.Request) {
// Set the content length on the request struct directly instead of the header.
// The content-length header gets reset by the golang http library before
// sendind out the request, resulting in chunked encoding to be used which
// breaks the quota checks in ocdav.
if method == "PUT" {
rq.ContentLength = u.Info.Size
}
})
f, err := os.Open(u.BinPath())
if err != nil {
return err
}
defer f.Close()
return client.WriteStream(rel, f, 0)
}
func (u *upload) cleanup() {
_ = os.Remove(u.BinPath())
_ = os.Remove(u.InfoPath())
}
func (u *upload) Terminate(ctx context.Context) error {
u.cleanup()
return nil
}
func (u *upload) ConcatUploads(_ context.Context, uploads []tusd.Upload) error {
file, err := os.OpenFile(u.BinPath(), os.O_WRONLY|os.O_APPEND, defaultFilePerm)
if err != nil {
return err
}
defer file.Close()
for _, partialUpload := range uploads {
fileUpload := partialUpload.(*upload)
src, err := os.Open(fileUpload.BinPath())
if err != nil {
return err
}
defer src.Close()
if _, err := io.Copy(file, src); err != nil {
return err
}
}
return nil
}
func (u *upload) DeclareLength(ctx context.Context, length int64) error {
u.Info.Size = length
u.Info.SizeIsDeferred = false
return nil
}
func (u *upload) checkHash(expected string, h hash.Hash) error {
if expected != hex.EncodeToString(h.Sum(nil)) {
return errtypes.ChecksumMismatch(fmt.Sprintf("invalid checksum: expected %s got %x", u.Info.MetaData["checksum"], h.Sum(nil)))
}
return nil
}
+6 -1
View File
@@ -18,6 +18,7 @@ type Validator interface {
// Policies represents a password validation rules
type Policies struct {
disabled bool
minCharacters int
minLowerCaseCharacters int
minUpperCaseCharacters int
@@ -29,8 +30,9 @@ type Policies struct {
}
// NewPasswordPolicy returns a new NewPasswordPolicy instance
func NewPasswordPolicy(minCharacters, minLowerCaseCharacters, minUpperCaseCharacters, minDigits, minSpecialCharacters int, bannedPasswordsList map[string]struct{}) Validator {
func NewPasswordPolicy(disabled bool, minCharacters, minLowerCaseCharacters, minUpperCaseCharacters, minDigits, minSpecialCharacters int, bannedPasswordsList map[string]struct{}) Validator {
p := &Policies{
disabled: disabled,
minCharacters: minCharacters,
minLowerCaseCharacters: minLowerCaseCharacters,
minUpperCaseCharacters: minUpperCaseCharacters,
@@ -46,6 +48,9 @@ func NewPasswordPolicy(minCharacters, minLowerCaseCharacters, minUpperCaseCharac
// Validate implements a password validation regarding the policy
func (s Policies) Validate(str string) error {
if s.disabled {
return nil
}
var allErr error
if !utf8.ValidString(str) {
return fmt.Errorf("the password contains invalid characters")