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

feat: removed command line flags, use environment variables instead

parent a038e0f9
Pipeline #9751 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.
......
......@@ -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
......
......@@ -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("", ""))
}
}
......@@ -11,6 +11,7 @@ import (
"strings"
"time"
"github.com/nicolaspernoud/vestibule/pkg/common"
"github.com/nicolaspernoud/vestibule/pkg/tokens"
)
......@@ -22,6 +23,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"`
......@@ -84,7 +96,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
}
......
......@@ -9,7 +9,10 @@ import (
"net/http"
"os"
"path"
"strconv"
"sync"
"github.com/nicolaspernoud/vestibule/pkg/log"
)
// Mutex used to lock file writing
......@@ -107,3 +110,37 @@ func Contains(a []string, x string) bool {
}
return false
}
// StringValueFromEnv set a value into an *interface from an environment variable or default
func StringValueFromEnv(ev string, def string) (string, error) {
val := os.Getenv(ev)
if val == "" {
return def, nil
}
return val, nil
}
// IntValueFromEnv set a value into an *interface from an environment variable or default
func IntValueFromEnv(ev string, def int) (int, error) {
val := os.Getenv(ev)
if val == "" {
return def, nil
}
return strconv.Atoi(val)
}
// BoolValueFromEnv set a value into an *interface from an environment variable or default
func BoolValueFromEnv(ev string, def bool) (bool, error) {
val := os.Getenv(ev)
if val == "" {
return def, nil
}
return strconv.ParseBool(val)
}
// CheckErrorFatal logs a fatal error
func CheckErrorFatal(err error) {
if err != nil {
log.Logger.Fatalf("Error : %v\n", err)
}
}
package common
import (
"os"
"testing"
)
func TestStringValueFromEnv(t *testing.T) {
os.Setenv("MY_EV", "from_env")
var rv string
var err error
type args struct {
ev string
def string
}
tests := []struct {
name string
args args
expected string
wantErr bool
}{
{"string_value_from_env", args{"MY_EV", "test"}, "from_env", false},
{"string_value_from_def", args{"MY_DEF", "test"}, "test", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if rv, err = StringValueFromEnv(tt.args.ev, tt.args.def); (err != nil) != tt.wantErr {
t.Errorf("StringValueFromEnv() error = %v, wantErr %v", err, tt.wantErr)
}
if rv != tt.expected {
t.Errorf("StringValueFromEnv() error ; got %v, expected %v", rv, tt.expected)
}
})
}
}
func TestIntValueFromEnv(t *testing.T) {
os.Setenv("MY_EV", "from_env")
var rv int
var err error
type args struct {
ev string
def int
}
tests := []struct {
name string
args args
expected int
wantErr bool
}{
{"int_value_from_def", args{"MY_DEF", 1}, 1, false},
{"string_on_int_from_env", args{"MY_EV", 1}, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if rv, err = IntValueFromEnv(tt.args.ev, tt.args.def); (err != nil) != tt.wantErr {
t.Errorf("IntValueFromEnv() error = %v, wantErr %v", err, tt.wantErr)
}
if rv != tt.expected {
t.Errorf("IntValueFromEnv() error ; got %v, expected %v", rv, tt.expected)
}
})
}
}
func TestBoolValueFromEnv(t *testing.T) {
os.Setenv("MY_EV", "from_env")
var rv bool
var err error
type args struct {
ev string
def bool
}
tests := []struct {
name string
args args
expected bool
wantErr bool
}{
{"bool_value_from_def", args{"MY_DEF", true}, true, false},
{"string_on_bool_from_def", args{"MY_EV", true}, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if rv, err = BoolValueFromEnv(tt.args.ev, tt.args.def); (err != nil) != tt.wantErr {
t.Errorf("BoolValueFromEnv() error = %v, wantErr %v", err, tt.wantErr)
}
if rv != tt.expected {
t.Errorf("BoolValueFromEnv() error ; got %v, expected %v", rv, tt.expected)
}
})
}
}
......@@ -7,6 +7,8 @@ import (
"os"
"text/template"
"time"
"github.com/nicolaspernoud/vestibule/pkg/common"
)
// HandleOpen open the main onlyoffice window
......@@ -17,10 +19,7 @@ func HandleOpen(fullHostname string) func(w http.ResponseWriter, req *http.Reque
http.Error(w, "could not open onlyoffice template: "+err.Error(), 500)
return
}
title := os.Getenv("ONLYOFFICE_TITLE")
if title == "" {
title = "OnlyOffice"
}
title, _ := common.StringValueFromEnv("ONLYOFFICE_TITLE", "VestibuleOffice")
p := struct {
Title string
OnlyOfficeServer string
......
#!/bin/bash
export $(cat .env | xargs)
go run main.go -apps=./configs/apps.json -davs=./configs/davs.json -https_port=1443
go run main.go
......@@ -32,7 +32,7 @@
<div class="navbar-brand">
<div class="navbar-item">
<a class="button is-primary is-rounded is-outlined" href="https://www.github.com/nicolaspernoud/Vestibule" target="_blank" rel="noopener noreferrer">
<span>4.3.57</span>
<span>4.4.0</span>
<span class="icon">
<svg
class="svg-inline--fa fa-github fa-w-16"
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment