mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-24 06:28:42 +01:00
feat!: removes legacy authentication model (#2633)
This commit is contained in:
@@ -21,9 +21,6 @@
|
|||||||
>
|
>
|
||||||
<mdi:cog />
|
<mdi:cog />
|
||||||
</router-link>
|
</router-link>
|
||||||
<a :href="`${base}/logout`" :title="$t('button.logout')" v-if="secured" class="btn btn-circle btn-sm">
|
|
||||||
<mdi:logout />
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
class="input input-sm mt-4 inline-flex cursor-pointer items-center gap-2 font-light hover:border-primary"
|
class="input input-sm mt-4 inline-flex cursor-pointer items-center gap-2 font-light hover:border-primary"
|
||||||
@@ -40,5 +37,5 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const { base, secured, hostname } = config;
|
const { hostname } = config;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -32,9 +32,6 @@
|
|||||||
<router-link :to="{ name: 'settings' }" class="btn btn-outline btn-sm">
|
<router-link :to="{ name: 'settings' }" class="btn btn-outline btn-sm">
|
||||||
<mdi:cog /> {{ $t("button.settings") }}
|
<mdi:cog /> {{ $t("button.settings") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<a class="btn btn-outline btn-sm" :href="`${base}/logout`" :title="$t('button.logout')" v-if="secured">
|
|
||||||
<mdi:logout /> {{ $t("button.logout") }}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="menu">
|
<ul class="menu">
|
||||||
@@ -56,7 +53,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const { base, secured } = config;
|
|
||||||
import { sessionHost } from "@/composable/storage";
|
import { sessionHost } from "@/composable/storage";
|
||||||
const store = useContainerStore();
|
const store = useContainerStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!authorizationNeeded">
|
<div>
|
||||||
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
|
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
|
||||||
<splitpanes @resized="onResized($event)">
|
<splitpanes @resized="onResized($event)">
|
||||||
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
|
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
|
||||||
@@ -71,7 +71,6 @@
|
|||||||
// @ts-ignore - splitpanes types are not available
|
// @ts-ignore - splitpanes types are not available
|
||||||
import { Splitpanes, Pane } from "splitpanes";
|
import { Splitpanes, Pane } from "splitpanes";
|
||||||
import { collapseNav } from "@/stores/settings";
|
import { collapseNav } from "@/stores/settings";
|
||||||
const { authorizationNeeded } = config;
|
|
||||||
|
|
||||||
const containerStore = useContainerStore();
|
const containerStore = useContainerStore();
|
||||||
const { activeContainers } = storeToRefs(containerStore);
|
const { activeContainers } = storeToRefs(containerStore);
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
import { Container } from "@/models/Container";
|
import { Container } from "@/models/Container";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { showToast } = useToast();
|
|
||||||
const { version } = config;
|
const { version } = config;
|
||||||
const containerStore = useContainerStore();
|
const containerStore = useContainerStore();
|
||||||
const { containers, ready } = storeToRefs(containerStore) as unknown as {
|
const { containers, ready } = storeToRefs(containerStore) as unknown as {
|
||||||
@@ -66,14 +65,6 @@ watchEffect(() => {
|
|||||||
setTitle(t("title.dashboard", { count: runningContainers.length }));
|
setTitle(t("title.dashboard", { count: runningContainers.length }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.secured) {
|
|
||||||
showToast({
|
|
||||||
title: "Deprecation Warning",
|
|
||||||
message: `The secured option is deprecated and will be removed in a future versions. See <a href="https://github.com/amir20/dozzle/issues/2630" target="_blank">this issue</a> for more information.`,
|
|
||||||
type: "warning",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
:deep(tr td) {
|
:deep(tr td) {
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ const text = document.querySelector("script#config__json")?.textContent || "{}";
|
|||||||
export interface Config {
|
export interface Config {
|
||||||
version: string;
|
version: string;
|
||||||
base: string;
|
base: string;
|
||||||
authorizationNeeded: boolean;
|
|
||||||
secured: boolean;
|
|
||||||
maxLogs: number;
|
maxLogs: number;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
hosts: { name: string; id: string }[];
|
hosts: { name: string; id: string }[];
|
||||||
|
|||||||
@@ -12,19 +12,6 @@ services:
|
|||||||
- 8080:8080
|
- 8080:8080
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
auth:
|
|
||||||
container_name: auth
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
environment:
|
|
||||||
- DOZZLE_FILTER=name=auth
|
|
||||||
- DOZZLE_USERNAME=foo
|
|
||||||
- DOZZLE_PASSWORD=bar
|
|
||||||
- DOZZLE_NO_ANALYTICS=1
|
|
||||||
ports:
|
|
||||||
- 9090:8080
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
simple-auth:
|
simple-auth:
|
||||||
container_name: simple-auth
|
container_name: simple-auth
|
||||||
volumes:
|
volumes:
|
||||||
@@ -82,5 +69,4 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- dozzle
|
- dozzle
|
||||||
- custom_base
|
- custom_base
|
||||||
- auth
|
|
||||||
- remote
|
- remote
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
|
||||||
|
|
||||||
test("authentication", async ({ page }) => {
|
|
||||||
await page.goto("http://auth:8080/");
|
|
||||||
await page.locator('input[name="username"]').fill("foo");
|
|
||||||
await page.locator('input[name="password"]').fill("bar");
|
|
||||||
await page.locator('button[type="submit"]').click();
|
|
||||||
await expect(page.getByTestId("containers")).toHaveText("Containers");
|
|
||||||
});
|
|
||||||
2
go.mod
2
go.mod
@@ -10,7 +10,6 @@ require (
|
|||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/gorilla/sessions v1.2.2
|
|
||||||
github.com/magiconair/properties v1.8.7
|
github.com/magiconair/properties v1.8.7
|
||||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
|
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
@@ -42,7 +41,6 @@ require (
|
|||||||
github.com/distribution/reference v0.5.0 // indirect
|
github.com/distribution/reference v0.5.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/go-cmp v0.5.5 // indirect
|
github.com/google/go-cmp v0.5.5 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
github.com/kr/pretty v0.2.1 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -45,12 +45,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
|
||||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
|
||||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
|||||||
@@ -1,106 +1,12 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var secured = false
|
|
||||||
var store *sessions.CookieStore
|
|
||||||
|
|
||||||
const authorityKey = "AUTH_TIMESTAMP"
|
|
||||||
const sessionName = "session"
|
|
||||||
|
|
||||||
func initializeAuth(h *handler) {
|
|
||||||
secured = false
|
|
||||||
if h.config.Username != "" && h.config.Password != "" {
|
|
||||||
store = sessions.NewCookieStore(generateSessionStorageKey(h.config.Username, h.config.Password))
|
|
||||||
store.Options.HttpOnly = true
|
|
||||||
store.Options.SameSite = http.SameSiteLaxMode
|
|
||||||
store.Options.MaxAge = 0
|
|
||||||
secured = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func authorizationRequired(next http.Handler) http.Handler {
|
|
||||||
if secured {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if isAuthorized(r) {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
} else {
|
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAuthorized(r *http.Request) bool {
|
|
||||||
if !secured {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
session, _ := store.Get(r, sessionName)
|
|
||||||
if session.IsNew {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := session.Values[authorityKey]; ok {
|
|
||||||
// TODO check for timestamp
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) isAuthorizationNeeded(r *http.Request) bool {
|
|
||||||
return secured && !isAuthorized(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) validateCredentials(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !secured {
|
|
||||||
log.Panic("Validating credentials without username and password should not happen")
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method != "POST" {
|
|
||||||
log.Fatal("Expecting credential validation method to be POST")
|
|
||||||
http.Error(w, http.StatusText(http.StatusNotAcceptable), http.StatusNotAcceptable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.ParseMultipartForm(4 * 1024); err != nil {
|
|
||||||
log.Fatalf("Error while parsing form data: %v", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := r.PostFormValue("username")
|
|
||||||
pass := r.PostFormValue("password")
|
|
||||||
session, _ := store.Get(r, sessionName)
|
|
||||||
|
|
||||||
if user == h.config.Username && pass == h.config.Password {
|
|
||||||
session.Values[authorityKey] = time.Now().Unix()
|
|
||||||
|
|
||||||
if err := session.Save(r, w); err != nil {
|
|
||||||
log.Fatalf("Error while parsing saving session: %v", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(http.StatusText(http.StatusOK)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) createToken(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) createToken(w http.ResponseWriter, r *http.Request) {
|
||||||
user := r.PostFormValue("username")
|
user := r.PostFormValue("username")
|
||||||
pass := r.PostFormValue("password")
|
pass := r.PostFormValue("password")
|
||||||
@@ -134,25 +40,3 @@ func (h *handler) deleteToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(http.StatusText(http.StatusOK)))
|
w.Write([]byte(http.StatusText(http.StatusOK)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) clearSession(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !secured {
|
|
||||||
log.Panic("Validating credentials with secured=false should not happen")
|
|
||||||
}
|
|
||||||
|
|
||||||
session, _ := store.Get(r, sessionName)
|
|
||||||
delete(session.Values, authorityKey)
|
|
||||||
|
|
||||||
if err := session.Save(r, w); err != nil {
|
|
||||||
log.Fatalf("Error while parsing saving session: %v", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, h.config.Base, http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSessionStorageKey(username string, password string) []byte {
|
|
||||||
key := sha256.Sum256([]byte(fmt.Sprintf("%s:%s", username, password)))
|
|
||||||
return key[:]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ func (h *handler) index(w http.ResponseWriter, req *http.Request) {
|
|||||||
if err == nil && req.URL.Path != "" && req.URL.Path != "/" {
|
if err == nil && req.URL.Path != "" && req.URL.Path != "/" {
|
||||||
fileServer.ServeHTTP(w, req)
|
fileServer.ServeHTTP(w, req)
|
||||||
} else {
|
} else {
|
||||||
if !isAuthorized(req) && req.URL.Path != "login" {
|
|
||||||
http.Redirect(w, req, path.Clean(h.config.Base+"/login"), http.StatusTemporaryRedirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.executeTemplate(w, req)
|
h.executeTemplate(w, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,14 +39,12 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
config := map[string]interface{}{
|
config := map[string]interface{}{
|
||||||
"base": base,
|
"base": base,
|
||||||
"version": h.config.Version,
|
"version": h.config.Version,
|
||||||
"authorizationNeeded": h.isAuthorizationNeeded(req),
|
"hostname": h.config.Hostname,
|
||||||
"secured": secured,
|
"hosts": hosts,
|
||||||
"hostname": h.config.Hostname,
|
"authProvider": h.config.Authorization.Provider,
|
||||||
"hosts": hosts,
|
"enableActions": h.config.EnableActions,
|
||||||
"authProvider": h.config.Authorization.Provider,
|
|
||||||
"enableActions": h.config.EnableActions,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user := auth.UserFromContext(req.Context())
|
user := auth.UserFromContext(req.Context())
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ type Config struct {
|
|||||||
Base string
|
Base string
|
||||||
Addr string
|
Addr string
|
||||||
Version string
|
Version string
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
Hostname string
|
Hostname string
|
||||||
NoAnalytics bool
|
NoAnalytics bool
|
||||||
Dev bool
|
Dev bool
|
||||||
@@ -79,8 +77,6 @@ func CreateServer(clients map[string]DockerClient, content fs.FS, config Config)
|
|||||||
var fileServer http.Handler
|
var fileServer http.Handler
|
||||||
|
|
||||||
func createRouter(h *handler) *chi.Mux {
|
func createRouter(h *handler) *chi.Mux {
|
||||||
initializeAuth(h)
|
|
||||||
|
|
||||||
base := h.config.Base
|
base := h.config.Base
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
@@ -101,7 +97,6 @@ func createRouter(h *handler) *chi.Mux {
|
|||||||
if h.config.Authorization.Provider != NONE {
|
if h.config.Authorization.Provider != NONE {
|
||||||
r.Use(auth.RequireAuthentication)
|
r.Use(auth.RequireAuthentication)
|
||||||
}
|
}
|
||||||
r.Use(authorizationRequired) // TODO remove this
|
|
||||||
r.Get("/api/logs/stream/{host}/{id}", h.streamLogs)
|
r.Get("/api/logs/stream/{host}/{id}", h.streamLogs)
|
||||||
r.Get("/api/logs/download/{host}/{id}", h.downloadLogs)
|
r.Get("/api/logs/download/{host}/{id}", h.downloadLogs)
|
||||||
r.Get("/api/logs/{host}/{id}", h.fetchLogsBetweenDates)
|
r.Get("/api/logs/{host}/{id}", h.fetchLogsBetweenDates)
|
||||||
@@ -112,7 +107,6 @@ func createRouter(h *handler) *chi.Mux {
|
|||||||
r.Get("/api/releases", h.releases)
|
r.Get("/api/releases", h.releases)
|
||||||
r.Get("/api/profile/avatar", h.avatar)
|
r.Get("/api/profile/avatar", h.avatar)
|
||||||
r.Patch("/api/profile", h.updateProfile)
|
r.Patch("/api/profile", h.updateProfile)
|
||||||
r.Get("/logout", h.clearSession) // TODO remove this
|
|
||||||
r.Get("/version", h.version)
|
r.Get("/version", h.version)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -127,7 +121,6 @@ func createRouter(h *handler) *chi.Mux {
|
|||||||
r.Delete("/api/token", h.deleteToken)
|
r.Delete("/api/token", h.deleteToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Post("/api/validateCredentials", h.validateCredentials) // TODO remove this
|
|
||||||
r.Get("/healthcheck", h.healthcheck)
|
r.Get("/healthcheck", h.healthcheck)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/magiconair/properties/assert"
|
"github.com/magiconair/properties/assert"
|
||||||
|
|
||||||
"github.com/amir20/dozzle/internal/docker"
|
|
||||||
"github.com/beme/abide"
|
"github.com/beme/abide"
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
@@ -49,19 +40,6 @@ func Test_createRoutes_redirect(t *testing.T) {
|
|||||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_createRoutes_redirect_with_auth(t *testing.T) {
|
|
||||||
fs := afero.NewMemMapFs()
|
|
||||||
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
|
|
||||||
|
|
||||||
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar", Username: "amir", Password: "password", Authorization: Authorization{Provider: NONE}})
|
|
||||||
req, err := http.NewRequest("GET", "/foobar/", nil)
|
|
||||||
require.NoError(t, err, "NewRequest should not return an error.")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_createRoutes_foobar(t *testing.T) {
|
func Test_createRoutes_foobar(t *testing.T) {
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("foo page"), 0644), "WriteFile should have no error.")
|
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("foo page"), 0644), "WriteFile should have no error.")
|
||||||
@@ -100,116 +78,3 @@ func Test_createRoutes_version(t *testing.T) {
|
|||||||
handler.ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_createRoutes_username_password(t *testing.T) {
|
|
||||||
|
|
||||||
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password", Authorization: Authorization{Provider: NONE}})
|
|
||||||
req, err := http.NewRequest("GET", "/", nil)
|
|
||||||
require.NoError(t, err, "NewRequest should not return an error.")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_createRoutes_username_password_invalid(t *testing.T) {
|
|
||||||
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password", Authorization: Authorization{Provider: NONE}})
|
|
||||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/123?stdout=1&stderr=1", nil)
|
|
||||||
require.NoError(t, err, "NewRequest should not return an error.")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_createRoutes_username_password_login_happy(t *testing.T) {
|
|
||||||
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password", Authorization: Authorization{Provider: NONE}})
|
|
||||||
|
|
||||||
body := &bytes.Buffer{}
|
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
|
|
||||||
fw, err := writer.CreateFormField("username")
|
|
||||||
require.NoError(t, err, "Creating field should not be error.")
|
|
||||||
_, err = io.Copy(fw, strings.NewReader("amir"))
|
|
||||||
require.NoError(t, err, "Copying field should not result in error.")
|
|
||||||
|
|
||||||
fw, err = writer.CreateFormField("password")
|
|
||||||
require.NoError(t, err, "Creating field should not be error.")
|
|
||||||
_, err = io.Copy(fw, strings.NewReader("password"))
|
|
||||||
require.NoError(t, err, "Copying field should not result in error.")
|
|
||||||
|
|
||||||
writer.Close()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", "/api/validateCredentials", bytes.NewReader(body.Bytes()))
|
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
||||||
|
|
||||||
require.NoError(t, err, "NewRequest should not return an error.")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
assert.Equal(t, rr.Code, 200)
|
|
||||||
cookie := rr.Header().Get("Set-Cookie")
|
|
||||||
assert.Matches(t, cookie, "session=.+")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_createRoutes_username_password_login_failed(t *testing.T) {
|
|
||||||
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password", Authorization: Authorization{Provider: NONE}})
|
|
||||||
|
|
||||||
body := &bytes.Buffer{}
|
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
|
|
||||||
fw, err := writer.CreateFormField("username")
|
|
||||||
require.NoError(t, err, "Creating field should not be error.")
|
|
||||||
_, err = io.Copy(fw, strings.NewReader("amir"))
|
|
||||||
require.NoError(t, err, "Copying field should not result in error.")
|
|
||||||
|
|
||||||
fw, err = writer.CreateFormField("password")
|
|
||||||
require.NoError(t, err, "Creating field should not be error.")
|
|
||||||
_, err = io.Copy(fw, strings.NewReader("bad"))
|
|
||||||
require.NoError(t, err, "Copying field should not result in error.")
|
|
||||||
|
|
||||||
writer.Close()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", "/api/validateCredentials", bytes.NewReader(body.Bytes()))
|
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
||||||
|
|
||||||
require.NoError(t, err, "NewRequest should not return an error.")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
assert.Equal(t, rr.Code, 401)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_createRoutes_username_password_valid_session(t *testing.T) {
|
|
||||||
mockedClient := new(MockedClient)
|
|
||||||
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
|
|
||||||
mockedClient.On("ContainerLogs", mock.Anything, "123", "", docker.STDALL).Return(io.NopCloser(strings.NewReader("test data")), io.EOF)
|
|
||||||
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password", Authorization: Authorization{Provider: NONE}})
|
|
||||||
|
|
||||||
// Get cookie first
|
|
||||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/123?stdout=1&stderr=1", nil)
|
|
||||||
require.NoError(t, err, "NewRequest should not return an error.")
|
|
||||||
session, _ := store.Get(req, sessionName)
|
|
||||||
session.Values[authorityKey] = time.Now().Unix()
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
session.Save(req, recorder)
|
|
||||||
cookies := recorder.Result().Cookies()
|
|
||||||
|
|
||||||
// Test with cookie
|
|
||||||
req, err = http.NewRequest("GET", "/api/logs/stream/localhost/123?stdout=1&stderr=1", nil)
|
|
||||||
require.NoError(t, err, "NewRequest should not return an error.")
|
|
||||||
req.AddCookie(cookies[0])
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_createRoutes_username_password_invalid_session(t *testing.T) {
|
|
||||||
mockedClient := new(MockedClient)
|
|
||||||
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
|
|
||||||
mockedClient.On("ContainerLogs", mock.Anything, "since", docker.STDALL).Return(io.NopCloser(strings.NewReader("test data")), io.EOF)
|
|
||||||
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password", Authorization: Authorization{Provider: NONE}})
|
|
||||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/123?stdout=1&stderr=1", nil)
|
|
||||||
require.NoError(t, err, "NewRequest should not return an error.")
|
|
||||||
req.AddCookie(&http.Cookie{Name: "session", Value: "baddata"})
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
assert.Equal(t, rr.Code, 401)
|
|
||||||
}
|
|
||||||
|
|||||||
17
main.go
17
main.go
@@ -44,8 +44,6 @@ type args struct {
|
|||||||
Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."`
|
Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."`
|
||||||
Username string `arg:"env:DOZZLE_USERNAME" help:"sets the username for auth."`
|
Username string `arg:"env:DOZZLE_USERNAME" help:"sets the username for auth."`
|
||||||
Password string `arg:"env:DOZZLE_PASSWORD" help:"sets password for auth"`
|
Password string `arg:"env:DOZZLE_PASSWORD" help:"sets password for auth"`
|
||||||
UsernameFile *DockerSecret `arg:"env:DOZZLE_USERNAME_FILE" help:"sets the secret path read username for auth."`
|
|
||||||
PasswordFile *DockerSecret `arg:"env:DOZZLE_PASSWORD_FILE" help:"sets the secret path read password for auth"`
|
|
||||||
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
|
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
|
||||||
WaitForDockerSeconds int `arg:"--wait-for-docker-seconds,env:DOZZLE_WAIT_FOR_DOCKER_SECONDS" help:"wait for docker to be available for at most this many seconds before starting the server."`
|
WaitForDockerSeconds int `arg:"--wait-for-docker-seconds,env:DOZZLE_WAIT_FOR_DOCKER_SECONDS" help:"wait for docker to be available for at most this many seconds before starting the server."`
|
||||||
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
|
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
|
||||||
@@ -193,8 +191,6 @@ func createServer(args args, clients map[string]web.DockerClient) *http.Server {
|
|||||||
Addr: args.Addr,
|
Addr: args.Addr,
|
||||||
Base: args.Base,
|
Base: args.Base,
|
||||||
Version: version,
|
Version: version,
|
||||||
Username: args.Username,
|
|
||||||
Password: args.Password,
|
|
||||||
Hostname: args.Hostname,
|
Hostname: args.Hostname,
|
||||||
NoAnalytics: args.NoAnalytics,
|
NoAnalytics: args.NoAnalytics,
|
||||||
Dev: dev,
|
Dev: dev,
|
||||||
@@ -274,19 +270,8 @@ func parseArgs() args {
|
|||||||
args.Filter[key] = append(args.Filter[key], val)
|
args.Filter[key] = append(args.Filter[key], val)
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.Username == "" && args.UsernameFile != nil {
|
|
||||||
args.Username = args.UsernameFile.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Password == "" && args.PasswordFile != nil {
|
|
||||||
args.Password = args.PasswordFile.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Username != "" || args.Password != "" {
|
if args.Username != "" || args.Password != "" {
|
||||||
log.Warn("Using --username and --password is being deprecated and removed in v6.x. Use --auth-provider instead. See https://dozzle.dev/guide/authentication#file-based-user-management for more information.")
|
log.Fatal("Using --username and --password is removed v6.x. See https://github.com/amir20/dozzle/issues/2630 for details.")
|
||||||
if args.Username == "" || args.Password == "" {
|
|
||||||
log.Fatalf("Username AND password are required for authentication")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user