mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-10-31 02:28:31 -04:00
feat(authguest): pin logic and filemanager upd
This commit is contained in:
1 parent
8e39386c60
commit
d40bdf6ecf
7 files changed
+221
-4
No files matched your search
@@ -9,6 +9,7 @@ require (
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/MicahParks/keyfunc/v2 v2.1.0
|
||||
github.com/Nerzal/gocloak/v13 v13.9.0
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/bbalet/stopwords v1.0.0
|
||||
github.com/beevik/etree v1.8.0
|
||||
github.com/blevesearch/bleve/v2 v2.6.1
|
||||
@@ -136,7 +137,6 @@ require (
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect
|
||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/alexedwards/argon2id v1.0.0 // indirect
|
||||
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 // indirect
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pin
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
)
|
||||
|
||||
const Length = 6
|
||||
|
||||
var ErrInvalidPin = errors.New("invalid pin")
|
||||
var ErrInvalidHash = errors.New("invalid pin hash")
|
||||
|
||||
func Generate() (string, error) {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(1000000))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not generate random pin: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%06d", n.Int64()), nil
|
||||
}
|
||||
|
||||
func Hash(p string) (string, error) {
|
||||
if p == "" {
|
||||
return "", ErrInvalidPin
|
||||
}
|
||||
|
||||
return argon2id.CreateHash(p, argon2id.DefaultParams)
|
||||
}
|
||||
|
||||
func Verify(candidate, hash string) (bool, error) {
|
||||
if hash == "" {
|
||||
return false, ErrInvalidHash
|
||||
}
|
||||
|
||||
match, err := argon2id.ComparePasswordAndHash(candidate, hash)
|
||||
if err != nil {
|
||||
if errors.Is(err, argon2id.ErrInvalidHash) {
|
||||
return false, ErrInvalidHash
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return match, nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pin
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
for range 50 {
|
||||
got, err := Generate()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, Length)
|
||||
for _, r := range got {
|
||||
assert.True(t, unicode.IsDigit(r))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateUnique(t *testing.T) {
|
||||
seen := make(map[string]struct{})
|
||||
for range 100 {
|
||||
got, err := Generate()
|
||||
require.NoError(t, err)
|
||||
seen[got] = struct{}{}
|
||||
}
|
||||
|
||||
assert.GreaterOrEqual(t, len(seen), 2)
|
||||
}
|
||||
|
||||
func TestHashProducesArgon2id(t *testing.T) {
|
||||
h, err := Hash("123456")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(h, "$argon2id$"))
|
||||
assert.NotEqual(t, "123456", h)
|
||||
}
|
||||
|
||||
func TestHashEmptyPin(t *testing.T) {
|
||||
_, err := Hash("")
|
||||
assert.ErrorIs(t, err, ErrInvalidPin)
|
||||
}
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
h, err := Hash("123456")
|
||||
require.NoError(t, err)
|
||||
|
||||
match, err := Verify("123456", h)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, match)
|
||||
|
||||
match, err = Verify("654321", h)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, match)
|
||||
}
|
||||
|
||||
func TestVerifyEmptyHash(t *testing.T) {
|
||||
_, err := Verify("123456", "")
|
||||
assert.ErrorIs(t, err, ErrInvalidHash)
|
||||
}
|
||||
|
||||
func TestVerifyMalformedHash(t *testing.T) {
|
||||
_, err := Verify("123456", "not-a-hash")
|
||||
assert.ErrorIs(t, err, ErrInvalidHash)
|
||||
}
|
||||
@@ -50,7 +50,18 @@ func (s *FileManager) Add(rec Record) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.add(rec)
|
||||
return s.upsert(rec)
|
||||
}
|
||||
|
||||
// Replace writes the record, overwriting an existing record for the same share id hash.
|
||||
func (s *FileManager) Replace(rec Record) error {
|
||||
lock, err := s.lockRecord(rec.ShareIDHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = lock.Unlock() }()
|
||||
|
||||
return s.upsert(rec)
|
||||
}
|
||||
|
||||
// Get returns the record for the given share id hash.
|
||||
@@ -100,7 +111,7 @@ func (s *FileManager) Redeem(shareIDHash string) error {
|
||||
}
|
||||
|
||||
rec.Redeemed = true
|
||||
return s.add(rec)
|
||||
return s.upsert(rec)
|
||||
}
|
||||
|
||||
func (s *FileManager) lockRecord(shareIDHash string) (*flock.Flock, error) {
|
||||
@@ -121,7 +132,7 @@ func (s *FileManager) lockRecord(shareIDHash string) (*flock.Flock, error) {
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
func (s *FileManager) add(rec Record) error {
|
||||
func (s *FileManager) upsert(rec Record) error {
|
||||
p, err := s.path(rec.ShareIDHash)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -39,6 +39,36 @@ func TestFileManagerAddGet(t *testing.T) {
|
||||
assert.Equal(t, rec, got)
|
||||
}
|
||||
|
||||
func TestFileManagerReplaceCreates(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
s := NewFileManager(dir)
|
||||
|
||||
rec := newRecord("e0123456-7890-abcd-ef01-234567890abc")
|
||||
require.NoError(t, s.Replace(rec))
|
||||
|
||||
got, err := s.Get(rec.ShareIDHash)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rec, got)
|
||||
}
|
||||
|
||||
func TestFileManagerReplaceOverwrites(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
s := NewFileManager(dir)
|
||||
|
||||
rec := newRecord("e0123456-7890-abcd-ef01-234567890abc")
|
||||
require.NoError(t, s.Add(rec))
|
||||
|
||||
rec.SecretHash = "other"
|
||||
rec.PinHash = "pinhash"
|
||||
rec.PinExpiry = time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC)
|
||||
rec.Redeemed = true
|
||||
require.NoError(t, s.Replace(rec))
|
||||
|
||||
got, err := s.Get(rec.ShareIDHash)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rec, got)
|
||||
}
|
||||
|
||||
func TestFileManagerGetMissing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
s := NewFileManager(dir)
|
||||
@@ -70,6 +100,7 @@ func TestFileManagerInvalidHash(t *testing.T) {
|
||||
|
||||
require.ErrorIs(t, s.Remove("ab"), ErrInvalidHash)
|
||||
require.ErrorIs(t, s.Add(Record{ShareIDHash: "ab"}), ErrInvalidHash)
|
||||
require.ErrorIs(t, s.Replace(Record{ShareIDHash: "ab"}), ErrInvalidHash)
|
||||
}
|
||||
|
||||
func TestFileManagerRemove(t *testing.T) {
|
||||
|
||||
@@ -248,3 +248,54 @@ func (_c *Manager_Remove_Call) RunAndReturn(run func(shareIDHash string) error)
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Replace provides a mock function for the type Manager
|
||||
func (_mock *Manager) Replace(rec storage.Record) error {
|
||||
ret := _mock.Called(rec)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Replace")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(storage.Record) error); ok {
|
||||
r0 = returnFunc(rec)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Manager_Replace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Replace'
|
||||
type Manager_Replace_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Replace is a helper method to define mock.On call
|
||||
// - rec storage.Record
|
||||
func (_e *Manager_Expecter) Replace(rec any) *Manager_Replace_Call {
|
||||
return &Manager_Replace_Call{Call: _e.mock.On("Replace", rec)}
|
||||
}
|
||||
|
||||
func (_c *Manager_Replace_Call) Run(run func(rec storage.Record)) *Manager_Replace_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 storage.Record
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(storage.Record)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Manager_Replace_Call) Return(err error) *Manager_Replace_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Manager_Replace_Call) RunAndReturn(run func(rec storage.Record) error) *Manager_Replace_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -17,12 +17,15 @@ type Record struct {
|
||||
ShareID string `json:"shareid"`
|
||||
ShareIDHash string `json:"shareidhash"`
|
||||
SecretHash string `json:"secrethash"`
|
||||
PinHash string `json:"pinhash"`
|
||||
Expiry time.Time `json:"expiry,omitzero"`
|
||||
PinExpiry time.Time `json:"pinexpiry,omitzero"`
|
||||
Redeemed bool `json:"redeemed"`
|
||||
}
|
||||
|
||||
type Manager interface {
|
||||
Add(rec Record) error
|
||||
Replace(rec Record) error
|
||||
Get(shareIDHash string) (Record, error)
|
||||
Remove(shareIDHash string) error
|
||||
Redeem(shareIDHash string) error
|
||||
|
||||
Reference in new issue
Block a user