Commit 33c62528 authored by Rémi PAILHAREY's avatar Rémi PAILHAREY
Browse files

Factorize rootmux + tests auth, rootmux

parent e52e3195
......@@ -12,7 +12,6 @@
"program": "${workspaceFolder}",
"env": {
"ADMIN_ROLE": "ADMINS",
"HOSTNAME": "localhost",
"INMEMORY_TOKEN_LIFE_DAYS": "2",
"LOGOUT_URL": "/"
}
......
......@@ -27,7 +27,6 @@ const (
var (
// AdminRole represents the role reserved for admins
AdminRole = common.StringValueFromEnv("ADMIN_ROLE", "ADMINS")
hostname = common.StringValueFromEnv("HOSTNAME", "localhost")
)
// User represents a logged in user
......@@ -64,16 +63,11 @@ func ValidateAuthMiddleware(next http.Handler, allowedRoles []string, checkXSRF
return
}
// Default to redirect to authentication
redirectTo := hostname
redirectTo := "/"
_, port, perr := net.SplitHostPort(r.Host)
if perr == nil {
redirectTo += ":" + port
}
// Write the requested url in a cookie
if r.Host != redirectTo {
cookie := http.Cookie{Name: "redirectAfterLogin", Domain: hostname, Value: r.Host + r.URL.Path, MaxAge: 10, Secure: true, HttpOnly: false, SameSite: http.SameSiteLaxMode}
http.SetCookie(w, &cookie)
}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusUnauthorized)
responseContent := fmt.Sprintf("error extracting token: %v<meta http-equiv=\"Refresh\" content=\"0; url=https://%v#login\"/>", err.Error(), redirectTo)
......@@ -119,7 +113,6 @@ func HandleLogout(w http.ResponseWriter, r *http.Request) {
// Delete the auth cookie
c := http.Cookie{
Name: authTokenKey,
Domain: hostname,
MaxAge: -1,
}
http.SetCookie(w, &c)
......
package auth
import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
"reflect"
"testing"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/common"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/tester"
)
var noH map[string]string
func TestCheckUserHasRole(t *testing.T) {
type args struct {
user TokenData
allowedRoles []string
}
tests := []struct {
name string
args args
wantErr bool
}{
{"has_all_roles", args{user: TokenData{User: User{Roles: []string{"role01", "role02"}}}, allowedRoles: []string{"role01", "role02"}}, false},
{"has_one_role", args{user: TokenData{User: User{Roles: []string{"role01", "role03"}}}, allowedRoles: []string{"role01", "role02"}}, false},
{"has_not_role", args{user: TokenData{User: User{Roles: []string{"role03", "role04"}}}, allowedRoles: []string{"role01", "role02"}}, true},
{"user_roles_are_empty", args{user: TokenData{User: User{Roles: []string{}}}, allowedRoles: []string{"role01", "role02"}}, true},
{"allowed_roles_are_empty", args{user: TokenData{User: User{Roles: []string{"role01", "role02"}}}, allowedRoles: []string{}}, true},
{"all_roles_are_empty", args{user: TokenData{User: User{Roles: []string{}}}, allowedRoles: []string{}}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := checkUserHasRole(tt.args.user, tt.args.allowedRoles); (err != nil) != tt.wantErr {
t.Errorf("checkUserHasRole() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestAddUser(t *testing.T) {
UsersFile = writeUsers()
defer os.Remove(UsersFile)
handler := http.HandlerFunc(AddUser)
// Alter the password of the admin user, must create an hash
tester.DoRequestOnHandler(t, handler, "POST", "/", noH, `{"id":"1","login":"admin","password": "password"}`, http.StatusOK, `[{"id":"1","login":"admin","memberOf":null,"passwordHash":"$2a`)
// Test that altering an user without altering the password keep the password hash
tester.DoRequestOnHandler(t, handler, "POST", "/", noH, `{"id":"1","login":"admin_altered"}`, http.StatusOK, `[{"id":"1","login":"admin_altered","memberOf":null,"passwordHash":"$2a`)
// Add a new user with a password, must pass
tester.DoRequestOnHandler(t, handler, "POST", "/", noH, `{"id":"3","login":"user3","password": "password_user3"}`, http.StatusOK, `[{"id":"1","login":"admin`)
// Add a new user with no password, must fail
tester.DoRequestOnHandler(t, handler, "POST", "/", noH, `{"id":"4","login":"user4","password": ""}`, http.StatusBadRequest, `password cannot be empty`)
}
func TestMatchUser(t *testing.T) {
UsersFile = "../../configs/users.json"
existingUser := User{ID: "2", Login: "user", Roles: []string{"USERS"}, PasswordHash: "$2a$10$PgiAoLxZhgNtr7kRK/DH5ezwT./7vRkWqFNEtJD1670z3Zf60HqgG"}
veryLongString, _ := common.GenerateRandomString(10000)
specialCharString := "\""
type args struct {
sentUser User
}
tests := []struct {
name string
args args
want User
wantErr bool
}{
{"user_exists", args{User{Login: "user", Password: "password"}}, existingUser, false},
{"user_does_not_exists", args{User{Login: "notuser", Password: "password"}}, User{}, true},
{"user_does_not_exists_and_wrong_password", args{User{Login: "notuser", Password: "wrongpassword"}}, User{}, true},
{"wrong_password", args{User{Login: "user", Password: "wrongpassword"}}, User{}, true},
{"no_password", args{User{Login: "user", Password: ""}}, User{}, true},
{"empty_user", args{User{Login: "", Password: "password"}}, User{}, true},
{"empty_user_and_password", args{User{Login: "", Password: ""}}, User{}, true},
{"very_long_string_as_user", args{User{Login: veryLongString, Password: "password"}}, User{}, true},
{"very_long_string_as_password", args{User{Login: "user", Password: veryLongString}}, User{}, true},
{"very_long_string_as_user_and_password", args{User{Login: veryLongString, Password: veryLongString}}, User{}, true},
{"special_char_string_as_user", args{User{Login: specialCharString, Password: "password"}}, User{}, true},
{"special_char_string_as_password", args{User{Login: "user", Password: specialCharString}}, User{}, true},
{"special_char_string_as_user_and_password", args{User{Login: specialCharString, Password: specialCharString}}, User{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := MatchUser(tt.args.sentUser)
if (err != nil) != tt.wantErr {
t.Errorf("MatchUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("MatchUser() = %v, want %v", got, tt.want)
}
})
}
}
func writeUsers() (name string) {
users := []*User{
{ID: "1", Login: "admin"},
{ID: "2", Login: "user"},
}
f, err := ioutil.TempFile("", "users")
if err != nil {
panic(err)
}
defer f.Close()
err = json.NewEncoder(f).Encode(users)
if err != nil {
panic(err)
}
return f.Name()
}
......@@ -71,7 +71,7 @@ func HandleInMemoryLogin(w http.ResponseWriter, r *http.Request) {
return
}
tokenData := TokenData{User: User{ID: user.ID, Login: user.Login, Email: user.Email, Roles: user.Roles}, XSRFToken: xsrfToken}
tokens.Manager.StoreData(tokenData, hostname, authTokenKey, tokenLifetime, w)
tokens.Manager.StoreData(tokenData, authTokenKey, tokenLifetime, w)
// Log the connexion
log.Logger.Printf("| %v (%v %v) | Login success | %v | %v", user.Login, user.Name, user.Surname, r.RemoteAddr, log.GetCityAndCountryFromRequest(r))
}
......
package auth
import (
"os"
"testing"
"time"
)
func TestSetTokenLifetime(t *testing.T) {
type args struct {
key string
value string
}
tests := []struct {
name string
args args
want time.Duration
}{
{"no environnement", args{"OTHER_ENV", "10"}, 24 * time.Hour},
{"wrong type", args{"INMEMORY_TOKEN_LIFE_DAYS", "A_STRING"}, 24 * time.Hour},
{"to small", args{"INMEMORY_TOKEN_LIFE_DAYS", "-1"}, 24 * time.Hour},
{"to big", args{"INMEMORY_TOKEN_LIFE_DAYS", "11 000"}, 24 * time.Hour},
{"ok", args{"INMEMORY_TOKEN_LIFE_DAYS", "3"}, 72 * time.Hour},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Setenv(tt.args.key, tt.args.value)
if got := setTokenLifetime(); got != tt.want {
t.Errorf("setTokenLifetime() = %v, want %v", got, tt.want)
}
})
}
}
package rootmux
import (
"net/http"
"os"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/auth"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/common"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/tokens"
"github.com/nicolaspernoud/vestibule/pkg/middlewares"
)
func CreateRootMux(staticDir string) http.Handler {
mainMux := http.NewServeMux()
mainMux.HandleFunc("/Logout", auth.HandleLogout)
mainMux.HandleFunc("/Login", auth.HandleInMemoryLogin)
mainMux.Handle("/", middlewares.NoCache(http.FileServer(&common.FallBackWrapper{Assets: http.Dir(staticDir)})))
// commonMux := http.NewServeMux()
mainMux.Handle("/api/common/WhoAmI", auth.ValidateAuthMiddleware(auth.WhoAmI(), []string{"*"}, false))
// mainMux.Handle("/api/common/", http.StripPrefix("/api/common", auth.ValidateAuthMiddleware(commonMux, []string{"*"}, true)))
adminMux := http.NewServeMux()
adminMux.HandleFunc("/newShareToken", tokens.NewShareToken)
adminMux.HandleFunc("/users/", auth.ProcessUsers)
mainMux.Handle("/api/admin/", http.StripPrefix("/api/admin", auth.ValidateAuthMiddleware(adminMux, []string{os.Getenv("ADMIN_ROLE")}, true)))
return mainMux
}
package rootmux
import (
"encoding/json"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"testing"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/auth"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/tester"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/tokens"
)
var (
// reg, _ = regexp.Compile("[\n \t]+")
// initialUsersBuff, _ = ioutil.ReadFile("../../configs/users.json")
// initialUsers = reg.ReplaceAllString(string(initialUsersBuff), "")
newUser = `{"id":"3","login":"new_user","memberOf":["USERS"],"password":"test"}`
noH map[string]string
)
func init() {
tokens.Init("../../configs/tokenskey.json", true)
}
func TestAll(t *testing.T) {
// Set the users file
auth.UsersFile = "../../configs/users.json"
os.Setenv("ADMIN_ROLE", "ADMINS")
os.Setenv("LOGOUT_URL", "/")
unloggedTests(t)
userTests(t)
adminTests(t)
}
func unloggedTests(t *testing.T) {
ts, do := createTester(t)
defer ts.Close()
// Try to get the users (must fail)
do("GET", "/api/admin/users/", noH, "", http.StatusUnauthorized, "error extracting token")
// Try to create an user (must fail)
do("POST", "/api/admin/users/", noH, newUser, http.StatusUnauthorized, "error extracting token")
// Try to delete an user (must fail)
do("DELETE", "/api/admin/users/0", noH, "", http.StatusUnauthorized, "error extracting token")
// Try to access the main page (must pass)
do("GET", "/", noH, "", http.StatusOK, "<!DOCTYPE html>")
// Try to get the user informations (must fail)
do("GET", "/api/common/WhoAmI", noH, "", http.StatusUnauthorized, "error extracting token")
// Do a in memory login with an unknown user
do("POST", "/Login", noH, `{"login": "unknownuser","password": "password"}`, http.StatusForbidden, `user not found`)
// Do a in memory login with a known user but bad password
do("POST", "/Login", noH, `{"login": "admin","password": "badpassword"}`, http.StatusForbidden, `user not found`)
// Try to create a new sharetoken (must fail)
do("POST", "/api/admin/newShareToken", noH, "", http.StatusUnauthorized, "error extracting token")
}
// TODO: Complete user and admin tests
func userTests(t *testing.T) {
ts, do := createTester(t)
defer ts.Close()
tests := func() {
// Get the XSRF Token
response := do("GET", "/api/common/WhoAmI", noH, "", http.StatusOK, "")
token := auth.TokenData{}
json.Unmarshal([]byte(response), &token)
xsrfHeader := map[string]string{"XSRF-TOKEN": token.XSRFToken}
// Try to get the users (must fail)
do("GET", "/api/admin/users/", xsrfHeader, "", http.StatusForbidden, "no user role among")
// Try to create an user (must fail)
do("POST", "/api/admin/users/", xsrfHeader, newUser, http.StatusForbidden, "no user role among")
// Try to delete an user (must fail)
do("DELETE", "/api/admin/users/0", xsrfHeader, "", http.StatusForbidden, "no user role among")
// Try to create a new sharetoken (must fail)
do("POST", "/api/admin/newShareToken", xsrfHeader, "", http.StatusForbidden, "no user role among")
}
// Do a in memory login with an known user
do("POST", "/Login", nil, `{"login": "user","password": "password"}`, http.StatusOK, "")
// Run the tests
tests()
// Try to logout (must pass)
do("GET", "/Logout", noH, "", http.StatusOK, "")
}
func adminTests(t *testing.T) {
ts, do := createTester(t)
defer ts.Close()
tests := func() {
// Get the XSRF Token
response := do("GET", "/api/common/WhoAmI", noH, "", http.StatusOK, "")
token := auth.TokenData{}
json.Unmarshal([]byte(response), &token)
xsrfHeader := map[string]string{"XSRF-TOKEN": token.XSRFToken}
// Try to get the users (must pass)
do("GET", "/api/admin/users/", xsrfHeader, "", http.StatusOK, `[{"id":"1",`)
// Try to create an user (must pass)
do("POST", "/api/admin/users/", xsrfHeader, newUser, http.StatusOK, `[{"id":"1",`)
// Try to delete an user (must pass)
do("DELETE", "/api/admin/users/0", xsrfHeader, "", http.StatusOK, `[{"id":"1",`)
// Try to create a new sharetoken (must pass)
do("POST", "/api/admin/newShareToken", xsrfHeader, "", http.StatusOK, "")
}
// Do a in memory login with an known user
do("POST", "/Login", nil, `{"login": "admin","password": "password"}`, http.StatusOK, "")
// Run the tests
tests()
// Try to logout (must pass)
do("GET", "/Logout", noH, "", http.StatusOK, "")
}
func createTester(t *testing.T) (*httptest.Server, func(method string, route string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string) {
// Create the server
mux := CreateRootMux("../../web")
ts := httptest.NewServer(mux)
url, _ := url.Parse(ts.URL)
port := url.Port()
// Create the cookie jar
jar, _ := cookiejar.New(nil)
// wrap the testing function
return ts, tester.CreateServerTester(t, "localhost", port, jar)
}
package tester
import (
"context"
"io/ioutil"
"net"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
)
// DoRequestOnHandler does a request on a router (or handler) and check the response
......@@ -26,3 +32,56 @@ func DoRequestOnHandler(t *testing.T, router http.Handler, method string, route
}
return string(rr.Body.String())
}
// DoRequestOnServer does a request on listening server
func DoRequestOnServer(t *testing.T, hostname string, port string, jar *cookiejar.Jar, method string, testURL string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string {
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}
// or create your own transport, there's an example on godoc.
http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, addr)
}
if strings.HasPrefix(testURL, "/") {
testURL = "http://" + hostname + ":" + port + testURL
} else {
u, _ := url.Parse("http://" + testURL)
testURL = "http://" + u.Host + ":" + port + u.Path + "?" + u.RawQuery
}
req, err := http.NewRequest(method, testURL, strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
for i, v := range headers {
req.Header.Set(i, v)
}
var client *http.Client
if jar != nil {
client = &http.Client{Jar: jar}
} else {
client = &http.Client{}
}
res, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
body, _ := ioutil.ReadAll(res.Body)
bodyString := string(body)
if status := res.StatusCode; status != expectedStatus {
t.Errorf("Tested %v %v %v ; handler returned wrong status code: got %v want %v", method, testURL, payload, status, expectedStatus)
}
if !strings.HasPrefix(bodyString, expectedBody) {
t.Errorf("Tested %v %v %v ; handler returned unexpected body: got %v want %v", method, testURL, payload, bodyString, expectedBody)
}
return bodyString
}
// CreateServerTester wraps DoRequestOnServer to factorize t, port and jar
func CreateServerTester(t *testing.T, hostname string, port string, jar *cookiejar.Jar) func(method string, route string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string {
return func(method string, url string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string {
return DoRequestOnServer(t, hostname, port, jar, method, url, headers, payload, expectedStatus, expectedBody)
}
}
......@@ -5,7 +5,7 @@ import (
"compress/flate"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
cryptorand "crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
......@@ -13,6 +13,7 @@ import (
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"strings"
"time"
......@@ -68,14 +69,14 @@ type Token struct {
}
// StoreData creates a token with the given data and returns it in a cookie
func (m manager) StoreData(data interface{}, hostName string, cookieName string, duration time.Duration, w http.ResponseWriter) {
func (m manager) StoreData(data interface{}, cookieName string, duration time.Duration, w http.ResponseWriter) {
expiration := now().Add(duration)
value, err := m.CreateToken(data, expiration)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
cookie := http.Cookie{Name: cookieName, Domain: hostName, Value: value, Expires: expiration, Secure: !m.debugMode, HttpOnly: true, SameSite: http.SameSiteLaxMode}
cookie := http.Cookie{Name: cookieName, Value: value, Expires: expiration, Secure: !m.debugMode, HttpOnly: true, SameSite: http.SameSiteLaxMode}
http.SetCookie(w, &cookie)
}
......@@ -196,7 +197,7 @@ func Encrypt(data []byte, key []byte) ([]byte, error) {
return []byte{}, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
if _, err = io.ReadFull(cryptorand.Reader, nonce); err != nil {
return []byte{}, err
}
cipherData := gcm.Seal(nonce, nonce, data, nil)
......@@ -221,3 +222,17 @@ func Decrypt(data []byte, key []byte) ([]byte, error) {
}
return plainData, nil
}
func NewShareToken(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
const possible = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"
const token_length = 64
b := make([]byte, token_length)
for i := range b {
b[i] = possible[rand.Intn(len(possible))]
}
fmt.Fprintf(w, string(b))
}
......@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"os"
"os/signal"
......@@ -14,28 +13,21 @@ import (
"syscall"
"time"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/auth"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/common"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/cuckoo"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/log"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/mail"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/rootmux"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/thehive"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/tokens"
"github.com/nicolaspernoud/vestibule/pkg/middlewares"
)
var (
i int
hostname string
logFile = common.StringValueFromEnv("LOG_FILE", "") // Optional file to log to
debugMode = flag.Bool("debug", false, "Debug mode, disable let's encrypt, enable CORS and more logging")
)
func init() {
defaultHostname := "cyber-signal"
hostname = common.StringValueFromEnv("HOSTNAME", defaultHostname)
}
func main() {
// Initialize logger
if logFile != "" {
......@@ -51,26 +43,12 @@ func main() {
}()
}
log.Logger.Println("--- Server is starting ---")
fullHostname := middlewares.GetFullHostname(hostname, 8080)
log.Logger.Println("Main hostname is ", fullHostname)
// Initializations
tokens.Init("./configs/tokenskey.json", *debugMode)
mainMux := http.NewServeMux()
mainMux.HandleFunc("/Logout", auth.HandleLogout)
mainMux.HandleFunc("/Login", auth.HandleInMemoryLogin)
mainMux.Handle("/", middlewares.NoCache(http.FileServer(&common.FallBackWrapper{Assets: http.Dir("web")})))
// commonMux := http.NewServeMux()
mainMux.Handle("/api/common/WhoAmI", auth.ValidateAuthMiddleware(auth.WhoAmI(), []string{"*"}, false))
// mainMux.Handle("/api/common/", http.StripPrefix("/api/common", auth.ValidateAuthMiddleware(commonMux, []string{"*"}, true)))
adminMux := http.NewServeMux()
adminMux.HandleFunc("/newToken", createNewToken)
adminMux.HandleFunc("/users/", auth.ProcessUsers)
mainMux.Handle("/api/admin/", http.StripPrefix("/api/admin", auth.ValidateAuthMiddleware(adminMux, []string{os.Getenv("ADMIN_ROLE")}, true)))
// Create main server
mainMux := rootmux.CreateRootMux("web")
http.ListenAndServe(":8080", mainMux)
// tags := make([]string, 1)
......@@ -109,20 +87,6 @@ func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello")
}
func createNewToken(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
const possible = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"
const token_length = 64
b := make([]byte, token_length)
for i := range b {
b[i] = possible[rand.Intn(len(possible))]
}
fmt.Fprintf(w, string(b))
}
// SaveFile is a function allowing to save locally the file in .msg format received. The file in .msg is receveid in binary stream.
func SaveFile(w http.ResponseWriter, r *http.Request) {
// file allows you to create a new file in a predefined directory
......
......@@ -39,8 +39,8 @@ class ShareToken {
});
this.token_textarea = document.getElementById("token-textarea");
this.copy_token_button = document.getElementById("token-copy");
this.copy_token_button.addEventListener("click", function () {
this.copy(token_textarea);
this.copy_token_button.addEventListener("click", () => {
this.copy(this.token_textarea);
});
}
......