Commit 94774465 authored by Nicolas Pernoud's avatar Nicolas Pernoud
Browse files

Merge branch 'development'

parents 712b58c7 ae82fc77
Pipeline #9883 passed with stages
in 3 minutes and 9 seconds
# Common settings
HOSTNAME=vestibule.127.0.0.1.nip.io
ADMIN_ROLE=ADMINS
LOG_FILE=./vestibule.log
DEBUG_MODE=true
HTTPS_PORT=1443
# Needed to user OAuth2 authentication :
REDIRECT_URL=https://${HOSTNAME}/OAuth2Callback
......
......@@ -25,9 +25,10 @@
"HOSTNAME": "vestibule.127.0.0.1.nip.io",
"ONLYOFFICE_TITLE": "VestibuleOffice",
"ONLYOFFICE_SERVER": "https://localhost:2443",
"INMEMORY_TOKEN_LIFE_DAYS": "2"
"INMEMORY_TOKEN_LIFE_DAYS": "2",
"DEBUG_MODE": "true",
"HTTPS_PORT": "1443"
},
"args": ["-debug", "-https_port=1443"],
"showLog": true
},
{
......@@ -50,9 +51,10 @@
"ADMIN_ROLE": "GGD_ORG_DG-DEES-DINSI-DAAG_TOUS",
"HOSTNAME": "vestibule.127.0.0.1.nip.io",
"ONLYOFFICE_TITLE": "VestibuleOffice",
"ONLYOFFICE_SERVER": "https://localhost:2443"
"ONLYOFFICE_SERVER": "https://localhost:2443",
"DEBUG_MODE": "true",
"HTTPS_PORT": "1443"
},
"args": ["-debug", "-https_port=1443"],
"showLog": true
},
{
......
......@@ -62,6 +62,32 @@ The mock ip geodatabase should be replaced with a real one from maxmind for real
## Usage
### Configuration
Configuration is done through environment variables. The meaning of the different environment variables is detailed here :
| Environment variable | Usage | Default |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | --------------------------- |
| HOSTNAME | Vestibule main hostname : needed to know when to respond with the main GUI instead of an application on a webdav service | |
| APPS_FILE | Apps configuration file path | "./configs/davs.json" |
| DAVS_FILE | Davs configuration file path | "./configs/davs.json" |
| LETS_CACHE_DIR | Let's Encrypt cache directory | "./letsencrypt_cache" |
| LOG_FILE | Optional file to log to | defaults to no file logging |
| HTTPS_PORT | HTTPS port to serve on | 443 |
| HTTP_PORT | HTTP port to serve on, only used for Let's Encrypt HTTP Challenge | 80 |
| DEBUG_MODE | Debug mode, disable Let's Encrypt, enable CORS and more logging | false |
| ADMIN_ROLE | Admin role | ADMINS |
| REDIRECT_URL | Redirect url used by the idp to handle the callback | |
| CLIENT_ID | Client id to authenticate with the IdP for OAuth2 authentication | |
| CLIENT_SECRET | Client id to authenticate with the IdP for OAuth2 authentication | |
| AUTH_URL | IdP's authentication URL | |
| TOKEN_URL | IdP's token URL | |
| USERINFO_URL | IdP's userinfo URL | |
| LOGOUT_URL | IdP's logout URL | |
| ONLYOFFICE_TITLE | Title used on the OnlyOffice document editor window | VestibuleOffice |
| ONLYOFFICE_SERVER | Url of the OnlyOffice document server used to edit documents | |
| INMEMORY_TOKEN_LIFE_DAYS | Lifetime of authentication tokens for local users | 1 |
### OIDC/OAuth2 configuration
The OIDC/OAuth2 provider is configured with environment variables. The user is recovered with the /userinfo endpoint (part of the Open Id Connect standard) with a standard OAuth2 dance.
......@@ -71,20 +97,7 @@ The users roles **must** be recovered in an "memberOf" claim array obtained when
### Mounting webdav share on your OS
Vestibule allow using the authentication token in an basic auth header to allow mounting webdavs.
Here is an example of a script login in the user and using the token from the cookie to mount webdav on Ubuntu Linux :
```bash
#/bin/bash
LOGIN_URL=https://vestibule.127.0.0.1.nip.io:1443/Login
DAV_URL=davs://userdav.vestibule.127.0.0.1.nip.io:1443
LOGIN=admin
PASSWORD=password
SENT_BODY="{\"login\":\"${LOGIN}\",\"password\":\"${PASSWORD}\"}"
HTTP_RESPONSE=$(curl -k --header "Content-Type: application/json" --request POST --data ${SENT_BODY} --silent --output /dev/null --cookie-jar - $LOGIN_URL)
TOKEN=$(echo $HTTP_RESPONSE | awk '{ print $NF }')
printf "1\n${LOGIN}\n${TOKEN}\n" | gio mount $DAV_URL
```
Vestibule allow using the login with the password **OR** the authentication token in an basic auth header to allow mounting webdavs.
### Override branding
......
......@@ -4,7 +4,6 @@ services:
vestibule-container:
image: vestibule
build: .
command: -debug
restart: unless-stopped
volumes:
- /etc/localtime:/etc/localtime:ro
......@@ -26,3 +25,4 @@ services:
- LOGOUT_URL=${LOGOUT_URL}
- ONLYOFFICE_TITLE=${ONLYOFFICE_TITLE}
- ONLYOFFICE_SERVER=${ONLYOFFICE_SERVER}
- DEBUG_MODE=${DEBUG_MODE}
......@@ -9,10 +9,10 @@ require (
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/oschwald/maxminddb-golang v1.8.0
github.com/secure-io/sio-go v0.3.1
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/net v0.0.0-20201224014010-6772e930b67b
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5
golang.org/x/sys v0.0.0-20201211090839-8ad439b19e0f
golang.org/x/sys v0.0.0-20201223074533-0d417f636930
golang.org/x/text v0.3.4 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
......
......@@ -142,8 +142,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
......@@ -202,8 +202,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 h1:lwlPPsmjDKK0J6eG6xDWd5XPehI0R024zxjDnw3esPA=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
......@@ -249,8 +249,8 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201211090839-8ad439b19e0f h1:QdHQnPce6K4XQewki9WNbG5KOROuDzqO3NaYjI1cXJ0=
golang.org/x/sys v0.0.0-20201211090839-8ad439b19e0f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
......
......@@ -79,7 +79,7 @@ func CreateRootMux(port int, appsFile string, davsFile string, staticDir string)
adminMux.HandleFunc("/davs/", davServer.ProcessDavs)
adminMux.HandleFunc("/users/", auth.ProcessUsers)
adminMux.HandleFunc("/sysinfo/", sysinfo.GetInfo)
mainMux.Handle("/api/admin/", http.StripPrefix("/api/admin", auth.ValidateAuthMiddleware(adminMux, []string{os.Getenv("ADMIN_ROLE")}, true)))
mainMux.Handle("/api/admin/", http.StripPrefix("/api/admin", auth.ValidateAuthMiddleware(adminMux, []string{auth.AdminRole}, true)))
// Serve static files falling back to serving index.html
mainMux.Handle("/", middlewares.NoCache(http.FileServer(&common.FallBackWrapper{Assets: http.Dir(staticDir)})))
// Put it together into the main handler
......
......@@ -17,6 +17,8 @@ import (
"github.com/nicolaspernoud/vestibule/pkg/tester"
"github.com/nicolaspernoud/vestibule/pkg/tokens"
b64 "encoding/base64"
"github.com/nicolaspernoud/vestibule/internal/mocks"
)
......@@ -30,7 +32,7 @@ var (
initialUsers = reg.ReplaceAllString(string(initialUsersBuff), "")
newUser = `{"id":"3","login":"new_user","memberOf":["USERS"],"password":"test"}`
newDav = `{"id": 5,"host":"writableadmindav.vestibule.io","root":"./testdata/data","secured":true,"writable":true,"roles":["ADMINS"]}`
noH = tester.Header{Key: "", Value: ""}
noH map[string]string
)
func init() {
......@@ -61,9 +63,10 @@ func TestAll(t *testing.T) {
userTests := createUserTests(t)
os.Setenv("USERINFO_URL", oAuth2Server.URL+"/admininfo")
adminTests := createAdminTests(t)
directWebdavTests := createDirectWebdavTests(t)
// RUN THE TESTS CONCURRENTLY
var wg sync.WaitGroup
functions := []func(wg *sync.WaitGroup){oauth2Tests, unloggedTests, userTests, adminTests}
functions := []func(wg *sync.WaitGroup){oauth2Tests, unloggedTests, userTests, adminTests, directWebdavTests}
for _, f := range functions {
wg.Add(1)
go f(&wg)
......@@ -156,7 +159,7 @@ func createUserTests(t *testing.T) func(wg *sync.WaitGroup) {
response := do("GET", "/api/common/WhoAmI", noH, "", 200, "")
token := auth.TokenData{}
json.Unmarshal([]byte(response), &token)
xsrfHeader := tester.Header{Key: "XSRF-TOKEN", Value: token.XSRFToken}
xsrfHeader := map[string]string{"XSRF-TOKEN": token.XSRFToken}
// Try to get the apps without XSRF token (must fail)
do("GET", "/api/admin/apps", noH, "", 401, "XSRF")
// Try to get the apps (must fail)
......@@ -198,19 +201,19 @@ func createUserTests(t *testing.T) func(wg *sync.WaitGroup) {
// Try to get the user informations (must pass)
do("GET", "/api/common/WhoAmI", xsrfHeader, "", 200, `{"id":`)
// Try to get a share token (must pass)
shareHeader := tester.Header{Key: "Authorization", Value: "Bearer " + do("POST", "/api/common/Share", xsrfHeader, `{"sharedfor":"guest","url":"userdav.vestibule.io/mydata/test.txt","lifespan":1,"readonly":true}`, 200, "")}
shareHeader := map[string]string{"Authorization": "Bearer " + do("POST", "/api/common/Share", xsrfHeader, `{"sharedfor":"guest","url":"userdav.vestibule.io/mydata/test.txt","lifespan":1,"readonly":true}`, 200, "")}
// Try get the specified resource without cookie (must fail)
doNoJar("GET", "userdav.vestibule.io/mydata/test.txt", xsrfHeader, "", 401, "error extracting token")
// Try to use the share token for the specified ressource (must pass)
doNoJar("GET", "userdav.vestibule.io/mydata/test.txt", shareHeader, "", 200, "This is a test")
// Try to use the share token for the specified ressource with query (must pass)
doNoJar("GET", "userdav.vestibule.io/mydata/test.txt?token="+url.QueryEscape(strings.TrimPrefix(shareHeader.Value, "Bearer ")), noH, "", 200, "This is a test")
doNoJar("GET", "userdav.vestibule.io/mydata/test.txt?token="+url.QueryEscape(strings.TrimPrefix(shareHeader["Authorization"], "Bearer ")), noH, "", 200, "This is a test")
// Try to use the readonly share token to alter the specified ressource (must fail)
doNoJar("PUT", "userdav.vestibule.io/mydata/test.txt", shareHeader, "Altered content", 403, "token is read only")
// Try to use the share token for an other ressource (must fail)*/
doNoJar("GET", "userdav.vestibule.io/mydata/another_test.txt", shareHeader, "", 401, "token restricted to url")
// Try to get a share token for a forbidden -admin only- resource (should pass, but token should be useless)
shareHeader = tester.Header{Key: "Authorization", Value: "Bearer " + do("POST", "/api/common/Share", xsrfHeader, `{"sharedfor":"guest","url":"admindav.vestibule.io/mydata/test.txt","lifespan":1,"readonly":true}`, 200, "")}
shareHeader = map[string]string{"Authorization": "Bearer " + do("POST", "/api/common/Share", xsrfHeader, `{"sharedfor":"guest","url":"admindav.vestibule.io/mydata/test.txt","lifespan":1,"readonly":true}`, 200, "")}
// Try to use the previous token for a forbidden resource (must fail)
doNoJar("GET", "admindav.vestibule.io/mydata/test.txt", shareHeader, "", 403, "no user role among")
}
......@@ -247,7 +250,7 @@ func createAdminTests(t *testing.T) func(wg *sync.WaitGroup) {
response := do("GET", "/api/common/WhoAmI", noH, "", 200, "")
token := auth.TokenData{}
json.Unmarshal([]byte(response), &token)
xsrfHeader := tester.Header{Key: "XSRF-TOKEN", Value: token.XSRFToken}
xsrfHeader := map[string]string{"XSRF-TOKEN": token.XSRFToken}
// Try to get the apps (must pass)
do("GET", "/api/admin/apps/", xsrfHeader, "", 200, "[{\"id\":1")
// Try to create an app without the XSRF-TOKEN (must fail)
......@@ -272,6 +275,8 @@ func createAdminTests(t *testing.T) func(wg *sync.WaitGroup) {
do("POST", "/api/admin/users/", xsrfHeader, `{"id":"4","login":"new_user","memberOf":["USERS"],"password":"test"}`, 400, `login already exists`)
// Try to delete an user (must pass)
do("DELETE", "/api/admin/users/3", xsrfHeader, "", 200, `[{"id":"1",`)
// Try to log with the deleted user (must fail)
do("POST", "/Login", noH, `{"login": "new_user","password": "test"}`, 403, "")
// Try to get the user informations (must pass)
do("GET", "/api/common/WhoAmI", xsrfHeader, "", 200, `{"id":`)
// Try to access an authorized dav (must pass)
......@@ -303,7 +308,46 @@ func createAdminTests(t *testing.T) func(wg *sync.WaitGroup) {
}
}
func createTester(t *testing.T) (*httptest.Server, func(method string, url string, header tester.Header, payload string, expectedStatus int, expectedBody string) string, func(method string, url string, header tester.Header, payload string, expectedStatus int, expectedBody string) string) {
/**
DIRECT WEBDAV TESTS (this tests are to check that a direct access webdav works)
**/
func createDirectWebdavTests(t *testing.T) func(wg *sync.WaitGroup) {
// Users
authFromUser := func(user auth.User) string {
data := user.Login + ":" + user.Password
return "Basic " + b64.StdEncoding.EncodeToString([]byte(data))
}
correctAuthHeader := map[string]string{"Authorization": authFromUser(auth.User{Login: "user", Password: "password"}), "User-Agent": "LibreOffice"}
wrongAuthHeader := map[string]string{"Authorization": authFromUser(auth.User{Login: "user", Password: "wrong_password"}), "User-Agent": "LibreOffice"}
wrongUserAgent := map[string]string{"Authorization": authFromUser(auth.User{Login: "user", Password: "password"}), "User-Agent": "Other"}
// Create the tester
ts, do, _ := createTester(t)
return func(wg *sync.WaitGroup) {
defer ts.Close() // Close the tester
defer wg.Done()
// Try to get the apps (must fail)
do("GET", "/api/admin/apps", correctAuthHeader, "", 403, "no user role among")
// Try to access the davs list (must pass)
do("GET", "/api/common/davs", correctAuthHeader, "", 200, "[{\"id\":1")
// Try to access a forbidden dav (must fail)
do("GET", "admindav.vestibule.io", correctAuthHeader, "", 403, "no user role among")
// Try to access the main page (must pass)
do("GET", "/", correctAuthHeader, "", 200, "<!DOCTYPE html>")
// Try to access an authorized app (must pass)
do("GET", "api.vestibule.io", correctAuthHeader, "", 200, "{")
// Try to access an authorized dav (must pass)
do("GET", "userdav.vestibule.io/mydata/test.txt?inline", correctAuthHeader, "", 200, "This is a test")
// Try to access a forbidden dav (must fail)
do("GET", "admindav.vestibule.io/mydata/test.txt", correctAuthHeader, "", 403, "no user role among")
// Try to access an authorized dav with wrong password (must fail)
do("GET", "userdav.vestibule.io/mydata/test.txt?inline", wrongAuthHeader, "", 401, "webdav client authentication")
// Try to access an authorized dav with wrong user agent (must fail)
do("GET", "userdav.vestibule.io/mydata/test.txt?inline", wrongUserAgent, "", 401, "error extracting token")
}
}
func createTester(t *testing.T) (*httptest.Server, func(method string, url string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string, func(method string, url string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string) {
// Create the server
mux := CreateRootMux(1443, "./testdata/apps.json", "./testdata/davs.json", "../../web")
ts := httptest.NewServer(mux.Mux)
......
......@@ -2,7 +2,6 @@ package main
import (
"crypto/tls"
"flag"
"fmt"
"net/http"
"os"
......@@ -11,6 +10,7 @@ import (
"syscall"
"time"
"github.com/nicolaspernoud/vestibule/pkg/common"
"github.com/nicolaspernoud/vestibule/pkg/middlewares"
"github.com/nicolaspernoud/vestibule/pkg/tokens"
......@@ -22,23 +22,33 @@ import (
)
var (
appsFile = flag.String("apps", "./configs/apps.json", "apps definition `file`")
davsFile = flag.String("davs", "./configs/davs.json", "davs definition `file`")
letsCacheDir = flag.String("letsencrypt_cache", "./letsencrypt_cache", "let's encrypt cache `directory`")
logFile = flag.String("log_file", "", "Optional file to log to, defaults to no file logging")
httpsPort = flag.Int("https_port", 443, "HTTPS port to serve on (defaults to 443)")
httpPort = flag.Int("http_port", 80, "HTTP port to serve on (defaults to 80), only used to get let's encrypt certificates")
debugMode = flag.Bool("debug", false, "Debug mode, disable let's encrypt, enable CORS and more logging")
appsFile, davsFile, letsCacheDir, logFile string
httpsPort, httpPort int
debugMode bool
)
func main() {
// Parse the flags
flag.Parse()
func init() {
var err error
appsFile, err = common.StringValueFromEnv("APPS_FILE", "./configs/apps.json") // Apps configuration file path
common.CheckErrorFatal(err)
davsFile, err = common.StringValueFromEnv("DAVS_FILE", "./configs/davs.json") // Davs configuration file path
common.CheckErrorFatal(err)
letsCacheDir, err = common.StringValueFromEnv("LETS_CACHE_DIR", "./letsencrypt_cache") // Let's Encrypt cache directory
common.CheckErrorFatal(err)
logFile, err = common.StringValueFromEnv("LOG_FILE", "") // Optional file to log to
common.CheckErrorFatal(err)
httpsPort, err = common.IntValueFromEnv("HTTPS_PORT", 443) // HTTPS port to serve on
common.CheckErrorFatal(err)
httpPort, err = common.IntValueFromEnv("HTTP_PORT", 80) // HTTP port to serve on, only used for Let's Encrypt HTTP Challenge
common.CheckErrorFatal(err)
debugMode, err = common.BoolValueFromEnv("DEBUG_MODE", false) // Debug mode, disable Let's Encrypt, enable CORS and more logging
common.CheckErrorFatal(err)
}
func main() {
// Initialize logger
if *logFile != "" {
log.SetFile(*logFile)
if logFile != "" {
log.SetFile(logFile)
// Properly close the log on exit
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
......@@ -50,19 +60,19 @@ func main() {
}()
}
log.Logger.Println("--- Server is starting ---")
fullHostname := middlewares.GetFullHostname(os.Getenv("HOSTNAME"), *httpsPort)
fullHostname := middlewares.GetFullHostname(os.Getenv("HOSTNAME"), httpsPort)
log.Logger.Println("Main hostname is ", fullHostname)
// Initializations
tokens.Init("./configs/tokenskey.json", *debugMode)
tokens.Init("./configs/tokenskey.json", debugMode)
// Create the server
rootMux := rootmux.CreateRootMux(*httpsPort, *appsFile, *davsFile, "web")
rootMux := rootmux.CreateRootMux(httpsPort, appsFile, davsFile, "web")
// Serve locally with https on debug mode or with let's encrypt on production mode
if *debugMode {
if debugMode {
// Init the hostname
mocks.Init(*httpsPort)
mocks.Init(httpsPort)
// Start a mock oauth2 server if debug mode is on
mockOAuth2Port := ":8090"
go http.ListenAndServe(mockOAuth2Port, mocks.CreateMockOAuth2())
......@@ -71,17 +81,17 @@ func main() {
mockAPIPort := ":8091"
go http.ListenAndServe(mockAPIPort, mocks.CreateMockAPI())
fmt.Println("Mock API server Listening on: http://localhost" + mockAPIPort)
log.Logger.Fatal(http.ListenAndServeTLS(":"+strconv.Itoa(*httpsPort), "./dev_certificates/localhost.crt", "./dev_certificates/localhost.key", log.Middleware(rootMux.Mux)))
log.Logger.Fatal(http.ListenAndServeTLS(":"+strconv.Itoa(httpsPort), "./dev_certificates/localhost.crt", "./dev_certificates/localhost.key", log.Middleware(rootMux.Mux)))
} else {
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(*letsCacheDir),
Cache: autocert.DirCache(letsCacheDir),
HostPolicy: rootMux.Policy,
}
server := &http.Server{
Addr: ":" + strconv.Itoa(*httpsPort),
Addr: ":" + strconv.Itoa(httpsPort),
Handler: rootMux.Mux,
TLSConfig: &tls.Config{
GetCertificate: certManager.GetCertificate,
......@@ -92,7 +102,7 @@ func main() {
IdleTimeout: 120 * time.Second,
}
go http.ListenAndServe(":"+strconv.Itoa(*httpPort), certManager.HTTPHandler(nil))
go http.ListenAndServe(":"+strconv.Itoa(httpPort), certManager.HTTPHandler(nil))
log.Logger.Fatal(server.ListenAndServeTLS("", ""))
}
}
# Constants
$LOGIN="user"
$PASSWORD="password"
$LOGIN_URL="https://vestibule.127.0.0.1.nip.io:1443/Login"
$FILES_URL= "https://admindav.vestibule.127.0.0.1.nip.io:1443"
# Mount
$body = "{`"login`":`"$LOGIN`",`"password`":`"$PASSWORD`"})"
Invoke-WebRequest -Uri $LOGIN_URL -Body $body -Method POST -SessionVariable websession -UseBasicParsing
$token = $websession.Cookies.GetCookies($LOGIN_URL)["auth_token"].value
$rename = New-Object -ComObject Shell.Application
net use T: $FILES_URL $token /user:$LOGIN /persistent:No
$rename.NameSpace("T:\").Self.Name = 'Test'
......@@ -14,8 +14,6 @@ import (
"github.com/nicolaspernoud/vestibule/pkg/tester"
)
var noH = tester.Header{Key: "", Value: ""}
func TestServer(t *testing.T) {
// Create the proxy target servers
target := httptest.NewServer(http.HandlerFunc(testHandler))
......@@ -62,22 +60,22 @@ func TestServer(t *testing.T) {
// Create tests
var tests = []struct {
url string
authHeader tester.Header
authHeader map[string]string
code int
body string
}{
{"http://test.proxy/", noH, 200, "OK"},
{"http://foo.test.proxy/", noH, 404, "Not found."},
{"http://footest.proxy/", noH, 404, "Not found."},
{"http://test.wildcard/", noH, 200, "OK"},
{"http://foo.test.wildcard/", noH, 200, "OK"},
{"http://test.static/", noH, 200, "contents of index.html"},
{"http://test.net/", noH, 404, "Not found."},
{"http://test.proxy/", nil, 200, "OK"},
{"http://foo.test.proxy/", nil, 404, "Not found."},
{"http://footest.proxy/", nil, 404, "Not found."},
{"http://test.wildcard/", nil, 200, "OK"},
{"http://foo.test.wildcard/", nil, 200, "OK"},
{"http://test.static/", nil, 200, "contents of index.html"},
{"http://test.net/", nil, 404, "Not found."},
}
// Run tests
for _, test := range tests {
tester.DoRequestOnHandler(t, s, "GET", test.url, test.authHeader, "", test.code, test.body)
tester.DoRequestOnHandler(t, s, "GET", test.url, nil, "", test.code, test.body)
}
// Create redirect tests
......
......@@ -2,6 +2,7 @@ package auth
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
......@@ -11,6 +12,7 @@ import (
"strings"
"time"
"github.com/nicolaspernoud/vestibule/pkg/common"
"github.com/nicolaspernoud/vestibule/pkg/tokens"
)
......@@ -22,6 +24,17 @@ const (
ContextData key = 0
)
var (
// AdminRole represents the role reserved for admins
AdminRole string
)
func init() {
var err error
AdminRole, err = common.StringValueFromEnv("ADMIN_ROLE", "ADMINS")
common.CheckErrorFatal(err)
}
// User represents a logged in user
type User struct {
ID string `json:"id,omitempty"`
......@@ -50,13 +63,17 @@ func ValidateAuthMiddleware(next http.Handler, allowedRoles []string, checkXSRF
roleChecker := func(w http.ResponseWriter, r *http.Request) {
user := TokenData{}
checkXSRF, err := tokens.Manager.ExtractAndValidateToken(r, authTokenKey, &user, checkXSRF)
if err != nil {
// Handle WebDav authentication
if strings.Contains(r.UserAgent(), "vfs") || strings.Contains(r.UserAgent(), "Microsoft-WebDAV") || strings.Contains(r.UserAgent(), "Konqueror") {
// Handle WebDav authentication
if err != nil && isWebdav(r.UserAgent()) {
// Test if the user password is directly given in the request, if so populate the user
user, err = getUserDirectly(r.Header.Get("Authorization"))
if err != nil {
w.Header().Set("WWW-Authenticate", `Basic realm="server"`)
http.Error(w, "webdav client authentication", 401)
return
}
}
if err != nil {
// Handle CORS preflight requests
if r.Method == "OPTIONS" {
return
......@@ -84,7 +101,7 @@ func ValidateAuthMiddleware(next http.Handler, allowedRoles []string, checkXSRF
http.Error(w, err.Error(), 403)
return
}
err = checkUserHasRole(user, []string{os.Getenv("ADMIN_ROLE")})
err = checkUserHasRole(user, []string{AdminRole})
if err == nil {
user.IsAdmin = true
}
......@@ -194,3 +211,31 @@ func GetTokenData(r *http.Request) (TokenData, error) {
}
return user, nil
}
// isWebdav works out if an user agent is a webdav user agent
func isWebdav(ua string) bool {
for _, a := range []string{"vfs", "Microsoft-WebDAV", "Konqueror", "LibreOffice", "Rei.Fs.WebDAV"} {
if strings.Contains(ua, a) {
return true
}
}
return false
}
// getUserDirectly directly checks if an user is allowed to connect
func getUserDirectly(authorizationHeader string) (TokenData, error) {
authHeader := strings.Split(authorizationHeader, " ")
var user User
if authHeader[0] == "Basic" && len(authHeader) == 2 {
decoded, err := base64.StdEncoding.DecodeString(authHeader[1])
if err == nil {
auth := strings.Split(string(decoded), ":")
sentUser := User{Login: auth[0], Password: auth[1]}
foundUser, err := MatchUser(sentUser)
if err == nil {
return (TokenData{User: foundUser}), nil
}
}
}
return TokenData{User: user}, errors.New("could not retrieve user directly from basic auth header")
}
......@@ -8,11 +8,13 @@ import (
"reflect"
"testing"
b64 "encoding/base64"
"github.com/nicolaspernoud/vestibule/pkg/common"
"github.com/nicolaspernoud/vestibule/pkg/tester"
)
var noH = tester.Header{Key: "", Value: ""}
var noH map[string]string
func TestCheckUserHasRole(t *testing.T) {
type args struct {
......@@ -108,3 +110,61 @@ func writeUsers() (name string) {
}
return f.Name()
}
func Test_isWebdav(t *testing.T) {
type args struct {
ua string
}
tests := []struct {
name string
args args
want bool
}{
{"is_exact", args{ua: "Microsoft-WebDAV"}, true},
{"contains", args{ua: "Contains-Microsoft-WebDAV-"}, true},
{"is_not", args{ua: "Microsoft-Other"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isWebdav(tt.args.ua); got != tt.want {
t.Errorf("isWebdav() = %v, want %v", got, tt.want)
}
})
}
}