Logout all sessions after changing password, enhanced unit tests

This commit is contained in:
Marc Ole Bulling
2021-05-10 21:45:31 +02:00
parent 1ff8cb2913
commit 8f02aeb881
15 changed files with 290 additions and 64 deletions
+1
View File
@@ -13,3 +13,4 @@ jobs:
with:
go-version: '^1.16.4'
- run: go test ./... --tags=test,awsmock
- run: go test ./... --tags=test,noaws
+1 -1
View File
@@ -1,6 +1,6 @@
# Gokapi
[![Go Report Card](https://goreportcard.com/badge/github.com/forceu/gokapi)](https://goreportcard.com/report/github.com/forceu/gokapi)
<a href='https://github.com/jpoles1/gopherbadger' target='_blank'>![gopherbadger-tag-do-not-edit](https://img.shields.io/badge/Go%20Coverage-87%25-brightgreen.svg?longCache=true&style=flat)</a>
<a href='https://github.com/jpoles1/gopherbadger' target='_blank'>![gopherbadger-tag-do-not-edit](https://img.shields.io/badge/Go%20Coverage-91%25-brightgreen.svg?longCache=true&style=flat)</a>
[![Docker Pulls](https://img.shields.io/docker/pulls/f0rc3/gokapi.svg)](https://hub.docker.com/r/f0rc3/gokapi/)
+2
View File
@@ -365,6 +365,8 @@ func addTrailingSlash(url string) string {
// DisplayPasswordReset shows a password prompt in the CLI and saves the new password
func DisplayPasswordReset() {
serverSettings.AdminPassword = HashPassword(askForPassword(), false)
// Log out all sessions
serverSettings.Sessions = make(map[string]models.Session)
save()
}
+5 -2
View File
@@ -50,7 +50,7 @@ func NewFile(fileContent io.Reader, fileHeader *multipart.FileHeader, uploadRequ
file.AwsBucket = settings.AwsBucket
settings.Files[id] = file
configuration.ReleaseAndSave()
if !aws.IsCredentialProvided() {
if !aws.IsCredentialProvided(false) {
if !helper.FileExists(dataDir + "/" + file.SHA256) {
destinationFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
@@ -190,7 +190,10 @@ func getFileHandler(file models.File, dataDir string) (*os.File, int64) {
func FileExists(file models.File, dataDir string) bool {
if file.AwsBucket != "" {
result, err := aws.FileExists(file)
helper.Check(err)
if err != nil {
fmt.Println("Warning, cannot check file " + file.Id + ": " + err.Error())
return true
}
return result
}
return helper.FileExists(dataDir + "/" + file.SHA256)
+36 -27
View File
@@ -124,13 +124,15 @@ func TestNewFile(t *testing.T) {
bigFile.Close()
os.Remove("bigfile")
testconfiguration.EnableS3()
file, err = NewFile(bytes.NewReader(content), &header, request)
test.IsNil(t, err)
test.IsEqualString(t, file.Name, "bigfile")
test.IsEqualString(t, file.SHA256, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
test.IsEqualString(t, file.Size, "20.0 MB")
testconfiguration.DisableS3()
if aws.IsAvailable {
testconfiguration.EnableS3()
file, err = NewFile(bytes.NewReader(content), &header, request)
test.IsNil(t, err)
test.IsEqualString(t, file.Name, "bigfile")
test.IsEqualString(t, file.SHA256, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
test.IsEqualString(t, file.Size, "20.0 MB")
testconfiguration.DisableS3()
}
}
func TestServeFile(t *testing.T) {
@@ -149,18 +151,19 @@ func TestServeFile(t *testing.T) {
test.IsNil(t, err)
test.IsEqualString(t, string(content), "This is a file for testing purposes")
testconfiguration.EnableS3()
r = httptest.NewRequest("GET", "/upload", nil)
w = httptest.NewRecorder()
file, result = GetFile("awsTest1234567890123")
test.IsEqualBool(t, result, true)
ServeFile(file, w, r, false)
test.ResponseBodyContains(t, w, "https://redirect.url")
testconfiguration.DisableS3()
if aws.IsAvailable {
testconfiguration.EnableS3()
r = httptest.NewRequest("GET", "/upload", nil)
w = httptest.NewRecorder()
file, result = GetFile("awsTest1234567890123")
test.IsEqualBool(t, result, true)
ServeFile(file, w, r, false)
test.ResponseBodyContains(t, w, "https://redirect.url")
testconfiguration.DisableS3()
}
}
func TestCleanUp(t *testing.T) {
testconfiguration.EnableS3()
settings := configuration.GetServerSettings()
configuration.Release()
test.IsEqualString(t, settings.Files["cleanuptest123456789"].Name, "cleanup")
@@ -226,12 +229,14 @@ func TestCleanUp(t *testing.T) {
test.IsEqualString(t, settings.Files["cleanuptest123456789"].Name, "")
test.IsEqualBool(t, helper.FileExists("test/data/2341354656543213246465465465432456898794"), false)
test.IsEqualString(t, settings.Files["awsTest1234567890123"].Name, "Aws Test File")
testconfiguration.DisableS3()
if aws.IsAvailable {
testconfiguration.EnableS3()
test.IsEqualString(t, settings.Files["awsTest1234567890123"].Name, "Aws Test File")
testconfiguration.DisableS3()
}
}
func TestDeleteFile(t *testing.T) {
testconfiguration.EnableS3()
testconfiguration.Create(true)
configuration.Load()
settings := configuration.GetServerSettings()
@@ -246,14 +251,18 @@ func TestDeleteFile(t *testing.T) {
test.IsEqualBool(t, result, false)
result = DeleteFile("")
test.IsEqualBool(t, result, false)
result, err := aws.FileExists(settings.Files["awsTest1234567890123"])
test.IsEqualBool(t, result, true)
test.IsNil(t, err)
DeleteFile("awsTest1234567890123")
result, err = aws.FileExists(settings.Files["awsTest1234567890123"])
test.IsEqualBool(t, result, false)
test.IsNil(t, err)
testconfiguration.DisableS3()
if aws.IsAvailable {
testconfiguration.EnableS3()
result, err := aws.FileExists(settings.Files["awsTest1234567890123"])
test.IsEqualBool(t, result, true)
test.IsNil(t, err)
DeleteFile("awsTest1234567890123")
result, err = aws.FileExists(settings.Files["awsTest1234567890123"])
test.IsEqualBool(t, result, false)
test.IsNil(t, err)
testconfiguration.DisableS3()
}
}
func createBigFile(name string, megabytes int64) {
+34 -12
View File
@@ -5,6 +5,7 @@ package aws
import (
"Gokapi/internal/models"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
@@ -16,14 +17,35 @@ import (
"time"
)
// IsCredentialProvided returns true if all credentials are provided, however does not check them to be valid
func IsCredentialProvided() bool {
// IsAvailable is true if Gokapi has been compiled with AWS support or the API is being mocked
const IsAvailable = true
// IsMockApi is true if the API is being mocked and therefore can only be used for testing purposes
const IsMockApi = false
// IsCredentialProvided returns true if all credentials are provided
func IsCredentialProvided(checkIfValid bool) bool {
requiredKeys := []string{"GOKAPI_AWS_BUCKET", "AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"}
for _, key := range requiredKeys {
if !isValidEnv(key) {
return false
}
}
if checkIfValid {
return checkIfValidLogin()
}
return true
}
func checkIfValidLogin() bool {
sess := session.Must(session.NewSession())
svc := s3.New(sess)
_, err := svc.Config.Credentials.Get()
if err != nil {
fmt.Println("WARNING: AWS S3 login not successful: " + err.Error())
return false
}
fmt.Println("AWS S3 login successful")
return true
}
@@ -34,8 +56,8 @@ func isValidEnv(key string) bool {
// Upload uploads a file to AWS
func Upload(input io.Reader, file models.File) (string, error) {
session := session.Must(session.NewSession())
uploader := s3manager.NewUploader(session)
sess := session.Must(session.NewSession())
uploader := s3manager.NewUploader(sess)
result, err := uploader.Upload(&s3manager.UploadInput{
Bucket: aws.String(file.AwsBucket),
@@ -50,8 +72,8 @@ func Upload(input io.Reader, file models.File) (string, error) {
// Download downloads a file from AWS
func Download(writer io.WriterAt, file models.File) (int64, error) {
session := session.Must(session.NewSession())
downloader := s3manager.NewDownloader(session)
sess := session.Must(session.NewSession())
downloader := s3manager.NewDownloader(sess)
size, err := downloader.Download(writer, &s3.GetObjectInput{
Bucket: aws.String(file.AwsBucket),
@@ -66,8 +88,8 @@ func Download(writer io.WriterAt, file models.File) (int64, error) {
// RedirectToDownload creates a presigned link that is valid for 15 seconds and redirects the
// client to this url
func RedirectToDownload(w http.ResponseWriter, r *http.Request, file models.File) error {
session := session.Must(session.NewSession())
s3svc := s3.New(session)
sess := session.Must(session.NewSession())
s3svc := s3.New(sess)
req, _ := s3svc.GetObjectRequest(&s3.GetObjectInput{
Bucket: aws.String(file.AwsBucket),
@@ -86,8 +108,8 @@ func RedirectToDownload(w http.ResponseWriter, r *http.Request, file models.File
// FileExists returns true if the object is stored in S3
func FileExists(file models.File) (bool, error) {
session := session.Must(session.NewSession())
svc := s3.New(session)
sess := session.Must(session.NewSession())
svc := s3.New(sess)
_, err := svc.HeadObject(&s3.HeadObjectInput{
Bucket: aws.String(file.AwsBucket),
@@ -108,8 +130,8 @@ func FileExists(file models.File) (bool, error) {
// DeleteObject deletes a file from S3
func DeleteObject(file models.File) (bool, error) {
session := session.Must(session.NewSession())
svc := s3.New(session)
sess := session.Must(session.NewSession())
svc := s3.New(sess)
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(file.AwsBucket),
+7 -1
View File
@@ -21,6 +21,12 @@ const (
accessKey = "accKey"
)
// IsAvailable is true if Gokapi has been compiled with AWS support or the API is being mocked
const IsAvailable = true
// IsMockApi is true if the API is being mocked and therefore can only be used for testing purposes
const IsMockApi = true
func isValidCredentials() bool {
requiredKeys := []string{"GOKAPI_AWS_BUCKET", "AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"}
requiredValues := []string{bucketName, region, accessId, accessKey}
@@ -34,7 +40,7 @@ func isValidCredentials() bool {
}
// IsCredentialProvided returns true if all credentials are provided, however does not check them to be valid
func IsCredentialProvided() bool {
func IsCredentialProvided(checkIfValid bool) bool {
requiredKeys := []string{"GOKAPI_AWS_BUCKET", "AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"}
for _, key := range requiredKeys {
if !isValidEnv(key) {
+7 -1
View File
@@ -12,8 +12,14 @@ import (
const errorString = "AWS not supported in this build"
// IsAvailable is true if Gokapi has been compiled with AWS support or the API is being mocked
const IsAvailable = false
// IsMockApi is true if the API is being mocked and therefore can only be used for testing purposes
const IsMockApi = false
// IsCredentialProvided returns true if all credentials are provided, however does not check them to be valid
func IsCredentialProvided() bool {
func IsCredentialProvided(checkIfValid bool) bool {
return false
}
+6 -4
View File
@@ -81,11 +81,13 @@ func TestIsCredentialProvided(t *testing.T) {
os.Unsetenv("AWS_REGION")
os.Unsetenv("AWS_ACCESS_KEY_ID")
os.Unsetenv("AWS_SECRET_ACCESS_KEY")
test.IsEqualBool(t, IsCredentialProvided(), false)
test.IsEqualBool(t, IsCredentialProvided(false), false)
os.Setenv("AWS_REGION", "valid")
test.IsEqualBool(t, IsCredentialProvided(), false)
test.IsEqualBool(t, IsCredentialProvided(false), false)
os.Setenv("AWS_ACCESS_KEY_ID", "valid")
test.IsEqualBool(t, IsCredentialProvided(), false)
test.IsEqualBool(t, IsCredentialProvided(false), false)
test.IsEqualBool(t, IsCredentialProvided(true), false)
os.Setenv("AWS_SECRET_ACCESS_KEY", "valid")
test.IsEqualBool(t, IsCredentialProvided(), true)
test.IsEqualBool(t, IsCredentialProvided(false), true)
test.IsEqualBool(t, IsCredentialProvided(true), true)
}
+10 -2
View File
@@ -100,6 +100,9 @@ func HttpPageResult(t MockT, config HttpTestConfig) []*http.Cookie {
for _, cookie := range config.Cookies {
req.Header.Set("Cookie", cookie.toString())
}
for _, header := range config.Headers {
req.Header.Set(header.Name, header.Value)
}
if len(config.PostValues) > 0 {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
@@ -107,8 +110,8 @@ func HttpPageResult(t MockT, config HttpTestConfig) []*http.Cookie {
resp, err := client.Do(req)
IsNil(t, err)
if resp.StatusCode != 200 {
t.Errorf("Status %d != 200", resp.StatusCode)
if resp.StatusCode != config.ResultCode {
t.Errorf("Status %d != %d", config.ResultCode, resp.StatusCode)
}
content, err := ioutil.ReadAll(resp.Body)
IsNil(t, err)
@@ -138,8 +141,10 @@ type HttpTestConfig struct {
Method string
PostValues []PostBody
Cookies []Cookie
Headers []Header
UploadFileName string
UploadFieldName string
ResultCode int
}
func (c *HttpTestConfig) init(t MockT) {
@@ -149,6 +154,9 @@ func (c *HttpTestConfig) init(t MockT) {
if c.Method == "" {
c.Method = "GET"
}
if c.ResultCode == 0 {
c.ResultCode = 200
}
}
// Cookie is a simple struct to pass cookie values for testing
@@ -5,6 +5,7 @@ package testconfiguration
import (
"Gokapi/internal/models"
"Gokapi/internal/storage/aws"
"fmt"
"os"
)
@@ -42,21 +43,33 @@ func Delete() {
// EnableS3 sets env variables for mock S3
func EnableS3() {
if !aws.IsMockApi {
fmt.Println("Warning, using real AWS S3 API! This test will fail if no valid credentials have been provided.")
fmt.Println("To mock the API, run test with --tags test,awsmock")
return
}
os.Setenv("GOKAPI_AWS_BUCKET", "gokapi-test")
os.Setenv("AWS_REGION", "mock-region-1")
os.Setenv("AWS_ACCESS_KEY_ID", "accId")
os.Setenv("AWS_SECRET_ACCESS_KEY", "accKey")
aws.Upload(nil, models.File{
awsFile := models.File{
Id: "awsTest1234567890123",
Name: "aws Test File",
Size: "20 MB",
SHA256: "x341354656543213246465465465432456898794",
AwsBucket: "gokapi-test",
})
}
aws.Upload(nil, awsFile)
// settings := configuration.GetServerSettings()
// settings.Files["awsTest1234567890123"] = awsFile
// configuration.Release()
}
// DisableS3 unsets env variables for mock S3
func DisableS3() {
if !aws.IsMockApi {
return
}
os.Unsetenv("GOKAPI_AWS_BUCKET")
os.Unsetenv("AWS_REGION")
os.Unsetenv("AWS_ACCESS_KEY_ID")
@@ -239,15 +252,15 @@ var configTestFile = []byte(`{
"ApiKeys":{
"validkey":{
"Id":"validkey",
"FriendlyName":"Unnamed Key",
"FriendlyName":"First Key",
"LastUsed":0,
"LastUsedString":""
},
"GAh1IhXDvYnqfYLazWBqMB9HSFmNPO":{
"Id":"GAh1IhXDvYnqfYLazWBqMB9HSFmNPO",
"FriendlyName":"Unnamed Key",
"LastUsed":0,
"LastUsedString":""
"FriendlyName":"Second Key",
"LastUsed":1620671580,
"LastUsedString":"used"
},
"jiREglQJW0bOqJakfjdVfe8T1EM8n8":{
"Id":"jiREglQJW0bOqJakfjdVfe8T1EM8n8",
@@ -2,6 +2,7 @@ package testconfiguration
import (
"Gokapi/internal/helper"
"Gokapi/internal/storage/aws"
"Gokapi/internal/test"
"os"
"testing"
@@ -32,3 +33,16 @@ func TestSetUpgradeConfigFile(t *testing.T) {
test.IsEqualBool(t, helper.FileExists(configFile), true)
TestDelete(t)
}
func TestEnableS3(t *testing.T) {
EnableS3()
if aws.IsMockApi {
test.IsEqualString(t, os.Getenv("AWS_REGION"), "mock-region-1")
}
}
func TestDisableS3S3(t *testing.T) {
DisableS3()
if aws.IsMockApi {
test.IsEqualString(t, os.Getenv("AWS_REGION"), "")
}
}
+2 -2
View File
@@ -169,7 +169,7 @@ func newApiKey(w http.ResponseWriter, r *http.Request) {
return
}
api.NewKey()
redirect(w, "./api")
redirect(w, "api")
}
// Handling of /apiDelete
@@ -182,7 +182,7 @@ func deleteApiKey(w http.ResponseWriter, r *http.Request) {
if ok {
api.DeleteKey(keys[0])
}
redirect(w, "./api")
redirect(w, "api")
}
// Handling of /api/
+146 -4
View File
@@ -408,9 +408,7 @@ func TestPostUpload(t *testing.T) {
})
}
func TestDeleteFile(t *testing.T) {
testconfiguration.EnableS3()
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/delete?id=e4TjE7CokWK0giiLNxDL",
IsHtml: true,
@@ -420,5 +418,149 @@ func TestDeleteFile(t *testing.T) {
Value: "validsession",
}},
})
testconfiguration.DisableS3()
}
}
func TestApiPageAuthorized(t *testing.T) {
t.Parallel()
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/api",
IsHtml: true,
RequiredContent: []string{"Click on the API key name to give it a new name."},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "validsession",
}},
})
}
func TestApiPageNotAuthorized(t *testing.T) {
t.Parallel()
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/api",
IsHtml: true,
RequiredContent: []string{"URL=./login"},
ExcludedContent: []string{"Click on the API key name to give it a new name."},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "invalid",
}},
})
}
func TestNewApiKey(t *testing.T) {
// Authorised
settings := configuration.GetServerSettings()
amountKeys := len(settings.ApiKeys)
configuration.Release()
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/apiNew",
IsHtml: true,
RequiredContent: []string{"URL=./api"},
ExcludedContent: []string{"URL=./login"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "validsession",
}},
})
settings = configuration.GetServerSettings()
amountKeysAfter := len(settings.ApiKeys)
configuration.Release()
test.IsEqualInt(t, amountKeysAfter, amountKeys+1)
test.IsEqualInt(t, amountKeysAfter, 5)
// Not authorised
amountKeys++
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/apiNew",
IsHtml: true,
RequiredContent: []string{"URL=./login"},
ExcludedContent: []string{"URL=./api"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "invalid",
}},
})
settings = configuration.GetServerSettings()
amountKeysAfter = len(settings.ApiKeys)
configuration.Release()
test.IsEqualInt(t, amountKeysAfter, amountKeys)
test.IsEqualInt(t, amountKeysAfter, 5)
}
func TestDeleteApiKey(t *testing.T) {
// Not authorised
settings := configuration.GetServerSettings()
amountKeys := len(settings.ApiKeys)
configuration.Release()
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/apiDelete?id=jiREglQJW0bOqJakfjdVfe8T1EM8n8",
IsHtml: true,
RequiredContent: []string{"URL=./login"},
ExcludedContent: []string{"URL=./api"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "invalid",
}},
})
settings = configuration.GetServerSettings()
amountKeysAfter := len(settings.ApiKeys)
test.IsEqualString(t, settings.ApiKeys["jiREglQJW0bOqJakfjdVfe8T1EM8n8"].Id, "jiREglQJW0bOqJakfjdVfe8T1EM8n8")
configuration.Release()
test.IsEqualInt(t, amountKeysAfter, amountKeys)
test.IsEqualInt(t, amountKeysAfter, 5)
// Authorised
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/apiDelete?id=jiREglQJW0bOqJakfjdVfe8T1EM8n8",
IsHtml: true,
RequiredContent: []string{"URL=./api"},
ExcludedContent: []string{"URL=./login"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "validsession",
}},
})
settings = configuration.GetServerSettings()
amountKeysAfter = len(settings.ApiKeys)
test.IsEmpty(t, settings.ApiKeys["jiREglQJW0bOqJakfjdVfe8T1EM8n8"].Id)
configuration.Release()
test.IsEqualInt(t, amountKeysAfter, amountKeys-1)
test.IsEqualInt(t, amountKeysAfter, 4)
}
func TestProcessApi(t *testing.T) {
// Not authorised
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/api/files/list",
RequiredContent: []string{"{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}"},
ExcludedContent: []string{"smallfile2"},
ResultCode: 401,
Cookies: []test.Cookie{{
Name: "session_token",
Value: "invalid",
}},
})
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/api/files/list",
RequiredContent: []string{"{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}"},
ExcludedContent: []string{"smallfile2"},
ResultCode: 401,
Headers: []test.Header{{"apikey", "invalid"}},
})
// Authorised
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/api/files/list",
RequiredContent: []string{"smallfile2"},
ExcludedContent: []string{"Unauthorized"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "validsession",
}},
})
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/api/files/list",
RequiredContent: []string{"smallfile2"},
ExcludedContent: []string{"Unauthorized"},
Headers: []test.Header{{"apikey", "validkey"}},
})
}
-2
View File
@@ -108,7 +108,6 @@ func TestChangeFriendlyName(t *testing.T) {
func TestDeleteFile(t *testing.T) {
settings := configuration.GetServerSettings()
configuration.Release()
testconfiguration.EnableS3()
w, r := getRecorder("GET", "/api/files/delete", nil, []test.Header{{
Name: "apikey",
Value: "validkey",
@@ -137,7 +136,6 @@ func TestDeleteFile(t *testing.T) {
Process(w, r, maxMemory)
test.IsEqualInt(t, w.Code, 200)
test.IsEqualString(t, settings.Files["jpLXGJKigM4hjtA6T6sN2"].Id, "")
testconfiguration.DisableS3()
}
func TestUpload(t *testing.T) {