Add link share password authentication (#831)
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/831 Co-authored-by: konrad <konrad@kola-entertainments.de> Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
@ -1512,3 +1512,61 @@ func (err ErrSubscriptionAlreadyExists) HTTPError() web.HTTPError {
|
||||
Message: "You're already subscribed.",
|
||||
}
|
||||
}
|
||||
|
||||
// =================
|
||||
// Link Share errors
|
||||
// =================
|
||||
|
||||
// ErrLinkSharePasswordRequired represents an error where a link share authentication requires a password and none was provided
|
||||
type ErrLinkSharePasswordRequired struct {
|
||||
ShareID int64
|
||||
}
|
||||
|
||||
// IsErrLinkSharePasswordRequired checks if an error is ErrLinkSharePasswordRequired.
|
||||
func IsErrLinkSharePasswordRequired(err error) bool {
|
||||
_, ok := err.(*ErrLinkSharePasswordRequired)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrLinkSharePasswordRequired) Error() string {
|
||||
return fmt.Sprintf("Link Share requires a password for authentication [ShareID: %d]", err.ShareID)
|
||||
}
|
||||
|
||||
// ErrCodeLinkSharePasswordRequired holds the unique world-error code of this error
|
||||
const ErrCodeLinkSharePasswordRequired = 13001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrLinkSharePasswordRequired) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeLinkSharePasswordRequired,
|
||||
Message: "This link share requires a password for authentication, but none was provided.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrLinkSharePasswordInvalid represents an error where a subscription entity type is unknown
|
||||
type ErrLinkSharePasswordInvalid struct {
|
||||
ShareID int64
|
||||
}
|
||||
|
||||
// IsErrLinkSharePasswordInvalid checks if an error is ErrLinkSharePasswordInvalid.
|
||||
func IsErrLinkSharePasswordInvalid(err error) bool {
|
||||
_, ok := err.(*ErrLinkSharePasswordInvalid)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrLinkSharePasswordInvalid) Error() string {
|
||||
return fmt.Sprintf("Provided Link Share password did not match the saved one [ShareID: %d]", err.ShareID)
|
||||
}
|
||||
|
||||
// ErrCodeLinkSharePasswordInvalid holds the unique world-error code of this error
|
||||
const ErrCodeLinkSharePasswordInvalid = 13002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrLinkSharePasswordInvalid) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusForbidden,
|
||||
Code: ErrCodeLinkSharePasswordInvalid,
|
||||
Message: "The provided link share password is invalid.",
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,11 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/web"
|
||||
@ -49,9 +52,12 @@ type LinkSharing struct {
|
||||
// The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
// The kind of this link. 0 = undefined, 1 = without password, 2 = with password (currently not implemented).
|
||||
// The kind of this link. 0 = undefined, 1 = without password, 2 = with password.
|
||||
SharingType SharingType `xorm:"bigint INDEX not null default 0" json:"sharing_type" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
// The password of this link share. You can only set it, not retrieve it after the link share has been created.
|
||||
Password string `xorm:"text null" json:"password"`
|
||||
|
||||
// The user who shared this list
|
||||
SharedBy *user.User `xorm:"-" json:"shared_by"`
|
||||
SharedByID int64 `xorm:"bigint INDEX not null" json:"-"`
|
||||
@ -129,7 +135,19 @@ func (share *LinkSharing) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
share.SharedByID = a.GetID()
|
||||
share.Hash = utils.MakeRandomString(40)
|
||||
|
||||
if share.Password != "" {
|
||||
share.SharingType = SharingTypeWithPassword
|
||||
share.Password, err = user.HashPassword(share.Password)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
share.SharingType = SharingTypeWithoutPassword
|
||||
}
|
||||
|
||||
_, err = s.Insert(share)
|
||||
share.Password = ""
|
||||
share.SharedBy, _ = user.GetFromAuth(a)
|
||||
return
|
||||
}
|
||||
@ -156,6 +174,7 @@ func (share *LinkSharing) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
if !exists {
|
||||
return ErrListShareDoesNotExist{ID: share.ID, Hash: share.Hash}
|
||||
}
|
||||
share.Password = ""
|
||||
return
|
||||
}
|
||||
|
||||
@ -212,6 +231,7 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
||||
|
||||
for _, s := range shares {
|
||||
s.SharedBy = users[s.SharedByID]
|
||||
s.Password = ""
|
||||
}
|
||||
|
||||
// Total count
|
||||
@ -287,3 +307,20 @@ func GetLinkSharesByIDs(s *xorm.Session, ids []int64) (shares map[int64]*LinkSha
|
||||
err = s.In("id", ids).Find(&shares)
|
||||
return
|
||||
}
|
||||
|
||||
// VerifyLinkSharePassword checks if a password of a link share matches a provided one.
|
||||
func VerifyLinkSharePassword(share *LinkSharing, password string) (err error) {
|
||||
if password == "" {
|
||||
return &ErrLinkSharePasswordRequired{ShareID: share.ID}
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(share.Password), []byte(password))
|
||||
if err != nil {
|
||||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
return &ErrLinkSharePasswordInvalid{ShareID: share.ID}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
140
pkg/models/link_sharing_test.go
Normal file
140
pkg/models/link_sharing_test.go
Normal file
@ -0,0 +1,140 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLinkSharing_Create(t *testing.T) {
|
||||
doer := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
Right: RightRead,
|
||||
}
|
||||
err := share.Create(s, doer)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, share.Hash)
|
||||
assert.NotEmpty(t, share.ID)
|
||||
assert.Equal(t, SharingTypeWithoutPassword, share.SharingType)
|
||||
db.AssertExists(t, "link_shares", map[string]interface{}{
|
||||
"id": share.ID,
|
||||
}, false)
|
||||
})
|
||||
t.Run("invalid right", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
Right: Right(123),
|
||||
}
|
||||
err := share.Create(s, doer)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidRight(err))
|
||||
})
|
||||
t.Run("password should be hashed", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
Right: RightRead,
|
||||
Password: "somePassword",
|
||||
}
|
||||
err := share.Create(s, doer)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, share.Hash)
|
||||
assert.NotEmpty(t, share.ID)
|
||||
assert.Empty(t, share.Password)
|
||||
db.AssertExists(t, "link_shares", map[string]interface{}{
|
||||
"id": share.ID,
|
||||
"sharing_type": SharingTypeWithPassword,
|
||||
}, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinkSharing_ReadAll(t *testing.T) {
|
||||
doer := &user.User{ID: 1}
|
||||
|
||||
t.Run("all no password", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
}
|
||||
all, _, _, err := share.ReadAll(s, doer, "", 1, -1)
|
||||
shares := all.([]*LinkSharing)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, shares, 2)
|
||||
for _, sharing := range shares {
|
||||
assert.Empty(t, sharing.Password)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinkSharing_ReadOne(t *testing.T) {
|
||||
doer := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ID: 1,
|
||||
}
|
||||
err := share.ReadOne(s, doer)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, share.Hash)
|
||||
assert.Equal(t, SharingTypeWithoutPassword, share.SharingType)
|
||||
})
|
||||
t.Run("with password", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ID: 4,
|
||||
}
|
||||
err := share.ReadOne(s, doer)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, share.Hash)
|
||||
assert.Equal(t, SharingTypeWithPassword, share.SharingType)
|
||||
assert.Empty(t, share.Password)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user