feat(authguest): pin logic and filemanager upd

This commit is contained in:
Alex Ababii committed 2026-10-13 09:30:52 +02:00
1 parent 8e39386c60
commit d40bdf6ecf
7 files changed
+221 -4

No files matched your search

+1 -1
View File
@@ -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