Skip to content

Commit 968ac68

Browse files
committed
add roles & login btn.
1 parent d014df4 commit 968ac68

File tree

11 files changed

+197
-417
lines changed

11 files changed

+197
-417
lines changed

Dockerfile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@
44
FROM golang:1.18-bullseye as builder
55
WORKDIR /app
66

7+
ARG VERSION
8+
ARG GIT_COMMIT
9+
ARG GIT_BRANCH
10+
711
COPY . .
8-
RUN go get -v -t -d .
9-
RUN /app/build
12+
RUN ./build
1013

1114
#
1215
# STAGE 2: build a small image
1316
#
1417
FROM scratch
1518
WORKDIR /app
1619

20+
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
1721
COPY --from=builder /app/bin /usr/bin/email-auth
1822
COPY tmpl tmpl
1923

20-
ENTRYPOINT [ "email-auth" ]
24+
ENTRYPOINT [ "/usr/bin/email-auth" ]
2125

2226
EXPOSE 8080
2327

cmd/root.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// go-base version 2.2.2
1+
// go-base version 2.3.0
22
package cmd
33

44
import (
@@ -190,6 +190,11 @@ func initConfiguration(cfgFile string, defCfgPath string, envPrefix string) []st
190190
}
191191
}
192192

193+
// expand environment variables
194+
for _, k := range viper.AllKeys() {
195+
viper.Set(k, expandStringValues(viper.Get(k)))
196+
}
197+
193198
return configs
194199
}
195200

@@ -417,3 +422,37 @@ func initLogging(config logConfig) {
417422
Int("maxbackups", config.MaxBackups).
418423
Msg("logging configured")
419424
}
425+
426+
func expandStringValues(value interface{}) interface{} {
427+
switch v := value.(type) {
428+
case string:
429+
return expandEnv(v)
430+
case []interface{}:
431+
nslice := make([]interface{}, 0, len(v))
432+
for _, vint := range v {
433+
nslice = append(nslice, expandStringValues(vint))
434+
}
435+
return nslice
436+
case map[string]interface{}:
437+
nmap := map[string]interface{}{}
438+
for mk, mv := range v {
439+
nmap[mk] = expandStringValues(mv)
440+
}
441+
return nmap
442+
default:
443+
return v
444+
}
445+
}
446+
447+
func expandEnv(s string) string {
448+
return os.Expand(s, func(str string) string {
449+
// This allows escaping environment variable substitution via $$, e.g.
450+
// - $FOO will be substituted with env var FOO
451+
// - $$FOO will be replaced with $FOO
452+
// - $$$FOO will be replaced with $ + substituted env var FOO
453+
if str == "$" {
454+
return "$"
455+
}
456+
return os.Getenv(str)
457+
})
458+
}

config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@ app:
1818

1919
# Users authentication using HTTP Basic Auth, with bcrypt hashes.
2020
# Generate entires using `htpasswd -Bbn username password`.
21+
# Use $$ to escape $ in password.
2122
users:
2223
- "username:$2y$05$jB7JXurhAS37c45mUwmvIO0vdLlzRVVlwJiQUY8eP2GL74TA5C0c2"
2324
# Path to a file, where additional users are stored, deparated by newline.
2425
# Generate file using `htpasswd -Bbn username password >> usersfile`.
2526
users_file: ""
2627

28+
# Users or email addresses that are administrators.
29+
roles:
30+
- "admin@test.com=admin" # user = role
31+
- "admin=admin"
32+
# Path to a file, where additional roles are stored, deparated by newline.
33+
roles_file: ""
34+
2735
# Allowed redirect URLs.
2836
redirect_allowlist:
2937
# Allow all HTTPs URLS.
@@ -37,11 +45,16 @@ app:
3745
# Allow host + path prefix.
3846
- "//test.com/foo"
3947

48+
# If login button should be shown, otherwise user is logged in automatically on visit.
49+
login_btn: true
50+
4051
header:
4152
# If authentication header should be enabled.
4253
enabled: true
4354
# Authentication header name.
4455
name: "X-Auth-User"
56+
# Authentication header role.
57+
role: "X-Auth-Role"
4558

4659
expiration:
4760
# Login link expiration in seconds. (default 5 min)

go.sum

Lines changed: 8 additions & 390 deletions
Large diffs are not rendered by default.

internal/config/config.go

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package config
22

33
import (
44
"fmt"
5-
"io/ioutil"
65
"net/url"
6+
"os"
77
"strings"
88
"time"
99

@@ -19,6 +19,7 @@ import (
1919
type Header struct {
2020
Enabled bool
2121
Name string
22+
Role string
2223
}
2324

2425
type Expiration struct {
@@ -32,11 +33,14 @@ type App struct {
3233
Target string
3334
Bind string
3435
Proxy bool
35-
Users map[string]string
36-
Emails []string
36+
Users map[string]string // username:password
37+
Emails []string // list of emails or @domains
38+
Roles map[string]string // username/email:role
3739

3840
RedirectAllowlist []url.URL
3941

42+
LoginBtn bool // do not login automatically but show login button instead
43+
4044
Header Header
4145
Expiration Expiration
4246
}
@@ -124,7 +128,7 @@ func (App) Init(cmd *cobra.Command) error {
124128
return err
125129
}
126130

127-
cmd.PersistentFlags().StringSlice("app.users", []string{}, "Users authentication using HTTP Basic Auth, with bcrypt hashes.")
131+
cmd.PersistentFlags().StringSlice("app.users", []string{}, "Users authentication using HTTP Basic Auth, with bcrypt hashes, in format user:hash.")
128132
if err := viper.BindPFlag("app.users", cmd.PersistentFlags().Lookup("app.users")); err != nil {
129133
return err
130134
}
@@ -134,11 +138,26 @@ func (App) Init(cmd *cobra.Command) error {
134138
return err
135139
}
136140

141+
cmd.PersistentFlags().StringSlice("app.roles", []string{}, "Roles for users and emails, in format key=value.")
142+
if err := viper.BindPFlag("app.roles", cmd.PersistentFlags().Lookup("app.roles")); err != nil {
143+
return err
144+
}
145+
146+
cmd.PersistentFlags().String("app.roles_file", "", "Path to a file, where additional roles are stored, deparated by newline.")
147+
if err := viper.BindPFlag("app.roles_file", cmd.PersistentFlags().Lookup("app.roles_file")); err != nil {
148+
return err
149+
}
150+
137151
cmd.PersistentFlags().StringSlice("app.redirect_allowlist", []string{}, "Allowed redirect URLs.")
138152
if err := viper.BindPFlag("app.redirect_allowlist", cmd.PersistentFlags().Lookup("app.redirect_allowlist")); err != nil {
139153
return err
140154
}
141155

156+
cmd.PersistentFlags().Bool("app.login_btn", false, "Show login button instead of automatic login.")
157+
if err := viper.BindPFlag("app.login_btn", cmd.PersistentFlags().Lookup("app.login_btn")); err != nil {
158+
return err
159+
}
160+
142161
//
143162
// header
144163
//
@@ -153,6 +172,11 @@ func (App) Init(cmd *cobra.Command) error {
153172
return err
154173
}
155174

175+
cmd.PersistentFlags().String("app.header.role", "X-Auth-Role", "Authentication header role.")
176+
if err := viper.BindPFlag("app.header.role", cmd.PersistentFlags().Lookup("app.header.role")); err != nil {
177+
return err
178+
}
179+
156180
//
157181
// expiration
158182
//
@@ -183,7 +207,7 @@ func (c *App) Set() {
183207
// load emails from a file
184208
emailsFile := viper.GetString("app.emails_file")
185209
if emailsFile != "" {
186-
emailsBytes, err := ioutil.ReadFile(emailsFile)
210+
emailsBytes, err := os.ReadFile(emailsFile)
187211
if err != nil {
188212
log.Panic().Err(err).Msgf("error opening emails file")
189213
}
@@ -198,7 +222,7 @@ func (c *App) Set() {
198222
// load users from a file
199223
usersFile := viper.GetString("app.users_file")
200224
if usersFile != "" {
201-
usersBytes, err := ioutil.ReadFile(usersFile)
225+
usersBytes, err := os.ReadFile(usersFile)
202226
if err != nil {
203227
log.Panic().Err(err).Msgf("error opening users file")
204228
}
@@ -207,6 +231,21 @@ func (c *App) Set() {
207231
strings.Split(string(usersBytes), "\n")...)
208232
}
209233

234+
// get roles from config
235+
roles := viper.GetStringSlice("app.roles")
236+
237+
// load roles from a file
238+
rolesFile := viper.GetString("app.roles_file")
239+
if rolesFile != "" {
240+
rolesBytes, err := os.ReadFile(rolesFile)
241+
if err != nil {
242+
log.Panic().Err(err).Msgf("error opening roles file")
243+
}
244+
245+
roles = append(roles,
246+
strings.Split(string(rolesBytes), "\n")...)
247+
}
248+
210249
// clean up emails
211250
c.Emails = []string{}
212251
for _, email := range emails {
@@ -235,10 +274,29 @@ func (c *App) Set() {
235274
c.Users[username] = secret
236275
}
237276

277+
// convert roles to a map
278+
c.Roles = map[string]string{}
279+
for _, row := range roles {
280+
row := strings.TrimSpace(row)
281+
if row == "" {
282+
continue
283+
}
284+
285+
split := strings.Split(row, "=")
286+
if len(split) != 2 {
287+
log.Panic().Msgf("error parsing role: %v", row)
288+
}
289+
290+
user, role := strings.TrimSpace(split[0]), strings.TrimSpace(split[1])
291+
user = strings.ToLower(user)
292+
c.Roles[user] = role
293+
}
294+
238295
log.Info().
239296
Int("emails", len(c.Emails)).
240297
Int("users", len(c.Users)).
241-
Msgf("loaded emails and users")
298+
Int("roles", len(c.Roles)).
299+
Msgf("loaded emails, users and roles")
242300

243301
urls := viper.GetStringSlice("app.redirect_allowlist")
244302
c.RedirectAllowlist = []url.URL{}
@@ -256,8 +314,11 @@ func (c *App) Set() {
256314
c.RedirectAllowlist = append(c.RedirectAllowlist, *parsed)
257315
}
258316

317+
c.LoginBtn = viper.GetBool("app.login_btn")
318+
259319
c.Header.Enabled = viper.GetBool("app.header.enabled")
260320
c.Header.Name = viper.GetString("app.header.name")
321+
c.Header.Role = viper.GetString("app.header.role")
261322

262323
c.Expiration.LoginLink = time.Duration(viper.GetInt64("app.expiration.link")) * time.Second
263324
c.Expiration.Session = time.Duration(viper.GetInt64("app.expiration.session")) * time.Second

internal/login.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func (l *login) linkAction(next http.HandlerFunc) http.HandlerFunc {
157157
logger := l.newLogger(r)
158158

159159
token := r.URL.Query().Get("token")
160-
if token == "" || r.Method != "GET" {
160+
if token == "" {
161161
next.ServeHTTP(w, r)
162162
return
163163
}
@@ -181,22 +181,35 @@ func (l *login) linkAction(next http.HandlerFunc) http.HandlerFunc {
181181
return
182182
}
183183

184-
newToken, err := l.auth.Login(token)
185-
if err != nil {
186-
logger.Err(err).Str("email", session.Email()).Msg("unable to login")
187-
l.page.Error(w, r, "Error while logging in, please contact your system administrator.", http.StatusInternalServerError)
184+
// log me in page
185+
if l.app.LoginBtn && r.Method == "GET" {
186+
l.page.LoginBtn(w, r)
188187
return
189188
}
190189

191-
l.setCookie(w, newToken)
190+
// login action
191+
if (l.app.LoginBtn && r.Method == "POST") || (!l.app.LoginBtn && r.Method == "GET") {
192+
newToken, err := l.auth.Login(token)
193+
if err != nil {
194+
logger.Err(err).Str("email", session.Email()).Msg("unable to login")
195+
l.page.Error(w, r, "Error while logging in, please contact your system administrator.", http.StatusInternalServerError)
196+
return
197+
}
198+
199+
l.setCookie(w, newToken)
200+
201+
to := r.URL.Query().Get("to")
202+
if !l.verifyRedirectLink(to) {
203+
to = l.app.Url
204+
}
192205

193-
to := r.URL.Query().Get("to")
194-
if !l.verifyRedirectLink(to) {
195-
to = l.app.Url
206+
logger.Info().Str("email", session.Email()).Str("to", to).Msg("login verified")
207+
http.Redirect(w, r, to, http.StatusTemporaryRedirect)
208+
return
196209
}
197210

198-
logger.Info().Str("email", session.Email()).Str("to", to).Msg("login verified")
199-
http.Redirect(w, r, to, http.StatusTemporaryRedirect)
211+
// invalid method
212+
next.ServeHTTP(w, r)
200213
}
201214
}
202215

internal/mail/email.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"crypto/tls"
66
"fmt"
77
"html/template"
8-
"io/ioutil"
8+
"os"
99

1010
"gopkg.in/mail.v2"
1111

@@ -19,7 +19,7 @@ type Manager struct {
1919
}
2020

2121
func New(templatePath string, app config.App, email config.Email) (*Manager, error) {
22-
html, err := ioutil.ReadFile(templatePath)
22+
html, err := os.ReadFile(templatePath)
2323
if err != nil {
2424
return nil, err
2525
}

0 commit comments

Comments
 (0)