diff --git a/common/helpers/tests.go b/common/helpers/tests.go index 09296ea2..cea9b730 100644 --- a/common/helpers/tests.go +++ b/common/helpers/tests.go @@ -40,10 +40,12 @@ func Diff(a, b interface{}) string { type HTTPEndpointCases []struct { Description string URL string + Header http.Header + JSONInput interface{} + ContentType string StatusCode int FirstLines []string - JSONInput interface{} JSONOutput interface{} } @@ -63,7 +65,13 @@ func TestHTTPEndpoints(t *testing.T, serverAddr net.Addr, cases HTTPEndpointCase var resp *http.Response var err error if tc.JSONInput == nil { - resp, err = http.Get(fmt.Sprintf("http://%s%s", serverAddr, tc.URL)) + req, _ := http.NewRequest("GET", + fmt.Sprintf("http://%s%s", serverAddr, tc.URL), + nil) + if tc.Header != nil { + req.Header = tc.Header + } + resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("GET %s:\n%+v", tc.URL, err) } @@ -73,8 +81,14 @@ func TestHTTPEndpoints(t *testing.T, serverAddr net.Addr, cases HTTPEndpointCase if err != nil { t.Fatalf("Encode() error:\n%+v", err) } - resp, err = http.Post(fmt.Sprintf("http://%s%s", serverAddr, tc.URL), - "application/json", payload) + req, _ := http.NewRequest("POST", + fmt.Sprintf("http://%s%s", serverAddr, tc.URL), + payload) + if tc.Header != nil { + req.Header = tc.Header + } + req.Header.Add("Content-Type", "application/json") + resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("POST %s:\n%+v", tc.URL, err) } diff --git a/console/data/avatars/accessorie_1.png b/console/data/avatars/accessorie_1.png new file mode 100644 index 00000000..8b0df115 Binary files /dev/null and b/console/data/avatars/accessorie_1.png differ diff --git a/console/data/avatars/accessorie_10.png b/console/data/avatars/accessorie_10.png new file mode 100644 index 00000000..aa3cd89e Binary files /dev/null and b/console/data/avatars/accessorie_10.png differ diff --git a/console/data/avatars/accessorie_11.png b/console/data/avatars/accessorie_11.png new file mode 100644 index 00000000..3dabc617 Binary files /dev/null and b/console/data/avatars/accessorie_11.png differ diff --git a/console/data/avatars/accessorie_12.png b/console/data/avatars/accessorie_12.png new file mode 100644 index 00000000..02a9600e Binary files /dev/null and b/console/data/avatars/accessorie_12.png differ diff --git a/console/data/avatars/accessorie_13.png b/console/data/avatars/accessorie_13.png new file mode 100644 index 00000000..bde3cb9d Binary files /dev/null and b/console/data/avatars/accessorie_13.png differ diff --git a/console/data/avatars/accessorie_14.png b/console/data/avatars/accessorie_14.png new file mode 100644 index 00000000..9c59c237 Binary files /dev/null and b/console/data/avatars/accessorie_14.png differ diff --git a/console/data/avatars/accessorie_15.png b/console/data/avatars/accessorie_15.png new file mode 100644 index 00000000..a61033d0 Binary files /dev/null and b/console/data/avatars/accessorie_15.png differ diff --git a/console/data/avatars/accessorie_16.png b/console/data/avatars/accessorie_16.png new file mode 100644 index 00000000..12561090 Binary files /dev/null and b/console/data/avatars/accessorie_16.png differ diff --git a/console/data/avatars/accessorie_17.png b/console/data/avatars/accessorie_17.png new file mode 100644 index 00000000..4d0f79e2 Binary files /dev/null and b/console/data/avatars/accessorie_17.png differ diff --git a/console/data/avatars/accessorie_18.png b/console/data/avatars/accessorie_18.png new file mode 100644 index 00000000..4d0f79e2 Binary files /dev/null and b/console/data/avatars/accessorie_18.png differ diff --git a/console/data/avatars/accessorie_19.png b/console/data/avatars/accessorie_19.png new file mode 100644 index 00000000..aac91a44 Binary files /dev/null and b/console/data/avatars/accessorie_19.png differ diff --git a/console/data/avatars/accessorie_2.png b/console/data/avatars/accessorie_2.png new file mode 100644 index 00000000..6c435f39 Binary files /dev/null and b/console/data/avatars/accessorie_2.png differ diff --git a/console/data/avatars/accessorie_20.png b/console/data/avatars/accessorie_20.png new file mode 100644 index 00000000..aac91a44 Binary files /dev/null and b/console/data/avatars/accessorie_20.png differ diff --git a/console/data/avatars/accessorie_3.png b/console/data/avatars/accessorie_3.png new file mode 100644 index 00000000..2aea9ab8 Binary files /dev/null and b/console/data/avatars/accessorie_3.png differ diff --git a/console/data/avatars/accessorie_4.png b/console/data/avatars/accessorie_4.png new file mode 100644 index 00000000..24d256d7 Binary files /dev/null and b/console/data/avatars/accessorie_4.png differ diff --git a/console/data/avatars/accessorie_5.png b/console/data/avatars/accessorie_5.png new file mode 100644 index 00000000..1cb53a13 Binary files /dev/null and b/console/data/avatars/accessorie_5.png differ diff --git a/console/data/avatars/accessorie_6.png b/console/data/avatars/accessorie_6.png new file mode 100644 index 00000000..24b1d6e6 Binary files /dev/null and b/console/data/avatars/accessorie_6.png differ diff --git a/console/data/avatars/accessorie_7.png b/console/data/avatars/accessorie_7.png new file mode 100644 index 00000000..cfdaaec7 Binary files /dev/null and b/console/data/avatars/accessorie_7.png differ diff --git a/console/data/avatars/accessorie_8.png b/console/data/avatars/accessorie_8.png new file mode 100644 index 00000000..b6cf1222 Binary files /dev/null and b/console/data/avatars/accessorie_8.png differ diff --git a/console/data/avatars/accessorie_9.png b/console/data/avatars/accessorie_9.png new file mode 100644 index 00000000..eb5a2ef6 Binary files /dev/null and b/console/data/avatars/accessorie_9.png differ diff --git a/console/data/avatars/body_1.png b/console/data/avatars/body_1.png new file mode 100644 index 00000000..f6ee3f35 Binary files /dev/null and b/console/data/avatars/body_1.png differ diff --git a/console/data/avatars/body_10.png b/console/data/avatars/body_10.png new file mode 100644 index 00000000..c5d8f099 Binary files /dev/null and b/console/data/avatars/body_10.png differ diff --git a/console/data/avatars/body_11.png b/console/data/avatars/body_11.png new file mode 100644 index 00000000..1013a693 Binary files /dev/null and b/console/data/avatars/body_11.png differ diff --git a/console/data/avatars/body_12.png b/console/data/avatars/body_12.png new file mode 100644 index 00000000..e550a0bd Binary files /dev/null and b/console/data/avatars/body_12.png differ diff --git a/console/data/avatars/body_13.png b/console/data/avatars/body_13.png new file mode 100644 index 00000000..5fda68a4 Binary files /dev/null and b/console/data/avatars/body_13.png differ diff --git a/console/data/avatars/body_14.png b/console/data/avatars/body_14.png new file mode 100644 index 00000000..0f012094 Binary files /dev/null and b/console/data/avatars/body_14.png differ diff --git a/console/data/avatars/body_15.png b/console/data/avatars/body_15.png new file mode 100644 index 00000000..985a640c Binary files /dev/null and b/console/data/avatars/body_15.png differ diff --git a/console/data/avatars/body_2.png b/console/data/avatars/body_2.png new file mode 100644 index 00000000..0c12c605 Binary files /dev/null and b/console/data/avatars/body_2.png differ diff --git a/console/data/avatars/body_3.png b/console/data/avatars/body_3.png new file mode 100644 index 00000000..dcaed91f Binary files /dev/null and b/console/data/avatars/body_3.png differ diff --git a/console/data/avatars/body_4.png b/console/data/avatars/body_4.png new file mode 100644 index 00000000..049dfa88 Binary files /dev/null and b/console/data/avatars/body_4.png differ diff --git a/console/data/avatars/body_5.png b/console/data/avatars/body_5.png new file mode 100644 index 00000000..fc86ba58 Binary files /dev/null and b/console/data/avatars/body_5.png differ diff --git a/console/data/avatars/body_6.png b/console/data/avatars/body_6.png new file mode 100644 index 00000000..172ba4b3 Binary files /dev/null and b/console/data/avatars/body_6.png differ diff --git a/console/data/avatars/body_7.png b/console/data/avatars/body_7.png new file mode 100644 index 00000000..448513aa Binary files /dev/null and b/console/data/avatars/body_7.png differ diff --git a/console/data/avatars/body_8.png b/console/data/avatars/body_8.png new file mode 100644 index 00000000..94ec54cd Binary files /dev/null and b/console/data/avatars/body_8.png differ diff --git a/console/data/avatars/body_9.png b/console/data/avatars/body_9.png new file mode 100644 index 00000000..ac0e9187 Binary files /dev/null and b/console/data/avatars/body_9.png differ diff --git a/console/data/avatars/eyes_1.png b/console/data/avatars/eyes_1.png new file mode 100644 index 00000000..9ffa6913 Binary files /dev/null and b/console/data/avatars/eyes_1.png differ diff --git a/console/data/avatars/eyes_10.png b/console/data/avatars/eyes_10.png new file mode 100644 index 00000000..43b64fb8 Binary files /dev/null and b/console/data/avatars/eyes_10.png differ diff --git a/console/data/avatars/eyes_11.png b/console/data/avatars/eyes_11.png new file mode 100644 index 00000000..d2048eb2 Binary files /dev/null and b/console/data/avatars/eyes_11.png differ diff --git a/console/data/avatars/eyes_12.png b/console/data/avatars/eyes_12.png new file mode 100644 index 00000000..5c90af52 Binary files /dev/null and b/console/data/avatars/eyes_12.png differ diff --git a/console/data/avatars/eyes_13.png b/console/data/avatars/eyes_13.png new file mode 100644 index 00000000..b38cb8e6 Binary files /dev/null and b/console/data/avatars/eyes_13.png differ diff --git a/console/data/avatars/eyes_14.png b/console/data/avatars/eyes_14.png new file mode 100644 index 00000000..5b0f7686 Binary files /dev/null and b/console/data/avatars/eyes_14.png differ diff --git a/console/data/avatars/eyes_15.png b/console/data/avatars/eyes_15.png new file mode 100644 index 00000000..02429ec5 Binary files /dev/null and b/console/data/avatars/eyes_15.png differ diff --git a/console/data/avatars/eyes_2.png b/console/data/avatars/eyes_2.png new file mode 100644 index 00000000..ee7f1ec6 Binary files /dev/null and b/console/data/avatars/eyes_2.png differ diff --git a/console/data/avatars/eyes_3.png b/console/data/avatars/eyes_3.png new file mode 100644 index 00000000..3ca8d351 Binary files /dev/null and b/console/data/avatars/eyes_3.png differ diff --git a/console/data/avatars/eyes_4.png b/console/data/avatars/eyes_4.png new file mode 100644 index 00000000..8fed3541 Binary files /dev/null and b/console/data/avatars/eyes_4.png differ diff --git a/console/data/avatars/eyes_5.png b/console/data/avatars/eyes_5.png new file mode 100644 index 00000000..22f14c77 Binary files /dev/null and b/console/data/avatars/eyes_5.png differ diff --git a/console/data/avatars/eyes_6.png b/console/data/avatars/eyes_6.png new file mode 100644 index 00000000..b7027fd2 Binary files /dev/null and b/console/data/avatars/eyes_6.png differ diff --git a/console/data/avatars/eyes_7.png b/console/data/avatars/eyes_7.png new file mode 100644 index 00000000..5781bc76 Binary files /dev/null and b/console/data/avatars/eyes_7.png differ diff --git a/console/data/avatars/eyes_8.png b/console/data/avatars/eyes_8.png new file mode 100644 index 00000000..207959a3 Binary files /dev/null and b/console/data/avatars/eyes_8.png differ diff --git a/console/data/avatars/eyes_9.png b/console/data/avatars/eyes_9.png new file mode 100644 index 00000000..f0c10821 Binary files /dev/null and b/console/data/avatars/eyes_9.png differ diff --git a/console/data/avatars/fur_1.png b/console/data/avatars/fur_1.png new file mode 100644 index 00000000..d8262055 Binary files /dev/null and b/console/data/avatars/fur_1.png differ diff --git a/console/data/avatars/fur_10.png b/console/data/avatars/fur_10.png new file mode 100644 index 00000000..1825ef11 Binary files /dev/null and b/console/data/avatars/fur_10.png differ diff --git a/console/data/avatars/fur_2.png b/console/data/avatars/fur_2.png new file mode 100644 index 00000000..f72ad222 Binary files /dev/null and b/console/data/avatars/fur_2.png differ diff --git a/console/data/avatars/fur_3.png b/console/data/avatars/fur_3.png new file mode 100644 index 00000000..3929323a Binary files /dev/null and b/console/data/avatars/fur_3.png differ diff --git a/console/data/avatars/fur_4.png b/console/data/avatars/fur_4.png new file mode 100644 index 00000000..b8a1ee39 Binary files /dev/null and b/console/data/avatars/fur_4.png differ diff --git a/console/data/avatars/fur_5.png b/console/data/avatars/fur_5.png new file mode 100644 index 00000000..2a027447 Binary files /dev/null and b/console/data/avatars/fur_5.png differ diff --git a/console/data/avatars/fur_6.png b/console/data/avatars/fur_6.png new file mode 100644 index 00000000..d41e1d21 Binary files /dev/null and b/console/data/avatars/fur_6.png differ diff --git a/console/data/avatars/fur_7.png b/console/data/avatars/fur_7.png new file mode 100644 index 00000000..87b2f6dd Binary files /dev/null and b/console/data/avatars/fur_7.png differ diff --git a/console/data/avatars/fur_8.png b/console/data/avatars/fur_8.png new file mode 100644 index 00000000..7c236a3d Binary files /dev/null and b/console/data/avatars/fur_8.png differ diff --git a/console/data/avatars/fur_9.png b/console/data/avatars/fur_9.png new file mode 100644 index 00000000..1593a3be Binary files /dev/null and b/console/data/avatars/fur_9.png differ diff --git a/console/data/avatars/mouth_1.png b/console/data/avatars/mouth_1.png new file mode 100644 index 00000000..f5ed92d7 Binary files /dev/null and b/console/data/avatars/mouth_1.png differ diff --git a/console/data/avatars/mouth_10.png b/console/data/avatars/mouth_10.png new file mode 100644 index 00000000..75ed1481 Binary files /dev/null and b/console/data/avatars/mouth_10.png differ diff --git a/console/data/avatars/mouth_2.png b/console/data/avatars/mouth_2.png new file mode 100644 index 00000000..4456fba6 Binary files /dev/null and b/console/data/avatars/mouth_2.png differ diff --git a/console/data/avatars/mouth_3.png b/console/data/avatars/mouth_3.png new file mode 100644 index 00000000..759a045e Binary files /dev/null and b/console/data/avatars/mouth_3.png differ diff --git a/console/data/avatars/mouth_4.png b/console/data/avatars/mouth_4.png new file mode 100644 index 00000000..04e9fddd Binary files /dev/null and b/console/data/avatars/mouth_4.png differ diff --git a/console/data/avatars/mouth_5.png b/console/data/avatars/mouth_5.png new file mode 100644 index 00000000..5ed617a6 Binary files /dev/null and b/console/data/avatars/mouth_5.png differ diff --git a/console/data/avatars/mouth_6.png b/console/data/avatars/mouth_6.png new file mode 100644 index 00000000..680eda78 Binary files /dev/null and b/console/data/avatars/mouth_6.png differ diff --git a/console/data/avatars/mouth_7.png b/console/data/avatars/mouth_7.png new file mode 100644 index 00000000..9fbf5970 Binary files /dev/null and b/console/data/avatars/mouth_7.png differ diff --git a/console/data/avatars/mouth_8.png b/console/data/avatars/mouth_8.png new file mode 100644 index 00000000..2a8407df Binary files /dev/null and b/console/data/avatars/mouth_8.png differ diff --git a/console/data/avatars/mouth_9.png b/console/data/avatars/mouth_9.png new file mode 100644 index 00000000..21751e78 Binary files /dev/null and b/console/data/avatars/mouth_9.png differ diff --git a/console/data/avatars/partlist.txt b/console/data/avatars/partlist.txt new file mode 100644 index 00000000..0ecc9219 --- /dev/null +++ b/console/data/avatars/partlist.txt @@ -0,0 +1,5 @@ +body +fur +eyes +mouth +accessorie diff --git a/console/data/docs/02-configuration.md b/console/data/docs/02-configuration.md index 44399af2..fbbbcafa 100644 --- a/console/data/docs/02-configuration.md +++ b/console/data/docs/02-configuration.md @@ -334,3 +334,27 @@ resolutions: The main components of the console service are `http` and `console`. `http` accepts the [same configuration](#http) as for the inlet service. The `console` has no configuration. + +### Authentication + +The console does not store user identities and is unable to +authenticate them. It expects an authenticating proxy will add some +headers to the API endpoints: + +- `X-Akvorado-User-Login` is the user login, +- `X-Akvorado-User-Name` is the user display name, +- `X-Akvorado-User-Email` is the user email address, +- `X-Akvorado-User-Avatar` is the user's avatar URL, +- `X-Akvorago-User-Logout` is a link to the logout link. + +Only the login header is mandatory. + +There are several systems providing user management with all the bells +and whistles, including OAuth2 support, multi-factor authentication +and API tokens. Here is a short selection of solutions able to act as +an authenticating reverse-proxy for Akvorado: + +- [Authentik](https://goauthentik.io/) +- [Authelia](https://www.authelia.com/) +- [Ory](https://www.ory.sh/) (no web interface provided in the open source edition) +- [Keycloak](https://www.keycloak.org/) diff --git a/console/frontend/src/components/ActiveUser.vue b/console/frontend/src/components/ActiveUser.vue new file mode 100644 index 00000000..0bbade08 --- /dev/null +++ b/console/frontend/src/components/ActiveUser.vue @@ -0,0 +1,57 @@ + + + diff --git a/console/frontend/src/components/NavigationBar.vue b/console/frontend/src/components/NavigationBar.vue index a3282f1d..6a89cebc 100644 --- a/console/frontend/src/components/NavigationBar.vue +++ b/console/frontend/src/components/NavigationBar.vue @@ -18,6 +18,7 @@
+ @@ -71,6 +72,7 @@ import { PresentationChartLineIcon, } from "@heroicons/vue/solid"; import DarkModeSwitcher from "@/components/DarkModeSwitcher.vue"; +import ActiveUser from "@/components/ActiveUser.vue"; const route = useRoute(); const navigation = computed(() => [ diff --git a/console/frontend/vite.config.js b/console/frontend/vite.config.js index b78f96c3..c12eedd8 100644 --- a/console/frontend/vite.config.js +++ b/console/frontend/vite.config.js @@ -22,6 +22,13 @@ export default defineConfig({ "/api": { target: "http://localhost:8080", changeOrigin: true, + headers: { + "X-Akvorado-User-Login": "alfred", + "X-Akvorado-User-Name": "Alfred Pennyworth", + "X-Akvorado-User-Email": "alfred@dccomics.example.com", + "X-Akvorado-User-Logout": + "https://en.wikipedia.org/wiki/Alfred_Pennyworth", + }, }, }, }, diff --git a/console/root.go b/console/root.go index efdb17d7..411a42df 100644 --- a/console/root.go +++ b/console/root.go @@ -75,16 +75,19 @@ func (c *Component) Start() error { c.r.Info().Msg("starting console component") c.d.HTTP.AddHandler("/", netHTTP.HandlerFunc(c.assetsHandlerFunc)) - c.d.HTTP.GinRouter.GET("/api/v0/console/docs/:name", c.docsHandlerFunc) - c.d.HTTP.GinRouter.GET("/api/v0/console/widget/flow-last", c.widgetFlowLastHandlerFunc) - c.d.HTTP.GinRouter.GET("/api/v0/console/widget/flow-rate", c.widgetFlowRateHandlerFunc) - c.d.HTTP.GinRouter.GET("/api/v0/console/widget/exporters", c.widgetExportersHandlerFunc) - c.d.HTTP.GinRouter.GET("/api/v0/console/widget/top/:name", c.widgetTopHandlerFunc) - c.d.HTTP.GinRouter.GET("/api/v0/console/widget/graph", c.widgetGraphHandlerFunc) - c.d.HTTP.GinRouter.POST("/api/v0/console/graph", c.graphHandlerFunc) - c.d.HTTP.GinRouter.POST("/api/v0/console/sankey", c.sankeyHandlerFunc) - c.d.HTTP.GinRouter.POST("/api/v0/console/filter/validate", c.filterValidateHandlerFunc) - c.d.HTTP.GinRouter.POST("/api/v0/console/filter/complete", c.filterCompleteHandlerFunc) + endpoint := c.d.HTTP.GinRouter.Group("/api/v0/console", c.userAuthentication()) + endpoint.GET("/docs/:name", c.docsHandlerFunc) + endpoint.GET("/widget/flow-last", c.widgetFlowLastHandlerFunc) + endpoint.GET("/widget/flow-rate", c.widgetFlowRateHandlerFunc) + endpoint.GET("/widget/exporters", c.widgetExportersHandlerFunc) + endpoint.GET("/widget/top/:name", c.widgetTopHandlerFunc) + endpoint.GET("/widget/graph", c.widgetGraphHandlerFunc) + endpoint.POST("/graph", c.graphHandlerFunc) + endpoint.POST("/sankey", c.sankeyHandlerFunc) + endpoint.POST("/filter/validate", c.filterValidateHandlerFunc) + endpoint.POST("/filter/complete", c.filterCompleteHandlerFunc) + endpoint.GET("/user/info", c.requireUserAuthentication(), c.userInfoHandlerFunc) + endpoint.GET("/user/avatar", c.requireUserAuthentication(), c.userAvatarHandlerFunc) c.t.Go(func() error { ticker := time.NewTicker(10 * time.Second) diff --git a/console/user.go b/console/user.go new file mode 100644 index 00000000..20e5b2c8 --- /dev/null +++ b/console/user.go @@ -0,0 +1,136 @@ +package console + +import ( + "bufio" + "embed" + "fmt" + "hash/fnv" + "image" + "image/draw" + "image/png" + "io/fs" + "math/rand" + "net/http" + "regexp" + + "github.com/gin-gonic/gin" +) + +var ( + //go:embed data/avatars + embeddedAvatarParts embed.FS + avatarRegexp = regexp.MustCompile(`^([a-z]+)_([0-9]+)\.png$`) +) + +// UserInformation contains information about the current user. +type userInformation struct { + Login string `json:"login" header:"X-Akvorado-User-Login" binding:"required"` + Name string `json:"name,omitempty" header:"X-Akvorado-User-Name"` + Email string `json:"email,omitempty" header:"X-Akvorado-User-Email" binding:"omitempty,email"` + AvatarURL string `json:"avatar-url" header:"X-Akvorado-User-Avatar" binding:"omitempty,uri"` + LogoutURL string `json:"logout-url,omitempty" header:"X-Akvorado-User-Logout" binding:"omitempty,uri"` +} + +// UserAuthentication is a middleware to fill information about the +// current user. It does not really perform authentication but relies +// on HTTP headers. +func (c *Component) userAuthentication() gin.HandlerFunc { + return func(gc *gin.Context) { + var info userInformation + if err := gc.ShouldBindHeader(&info); err != nil { + gc.Next() + return + } + if info.AvatarURL == "" { + info.AvatarURL = "/api/v0/console/user/avatar" + } + gc.Set("user", info) + gc.Next() + } +} + +// requireUserAuthentication requires user to be logged in. It returns +// 401 if not. +func (c *Component) requireUserAuthentication() gin.HandlerFunc { + return func(gc *gin.Context) { + _, ok := gc.Get("user") + if !ok { + gc.JSON(http.StatusUnauthorized, gin.H{"message": "No user logged in."}) + gc.Abort() + return + } + gc.Next() + } +} + +// userInfoHandlerFunc returns the information about the currently logged user. +func (c *Component) userInfoHandlerFunc(gc *gin.Context) { + info := gc.MustGet("user").(userInformation) + gc.JSON(http.StatusOK, info) +} + +// userAvatarHandlerFunc returns an avatar for the currently logger user. +func (c *Component) userAvatarHandlerFunc(gc *gin.Context) { + avatarParts := c.embedOrLiveFS(embeddedAvatarParts, "data/avatars") + + // Hash user login as a source + info := gc.MustGet("user").(userInformation) + hash := fnv.New64() + hash.Write([]byte(info.Login)) + randSource := rand.New(rand.NewSource(int64(hash.Sum64()))) + + // Grab list of parts + parts := []string{} + partList, err := avatarParts.Open("partlist.txt") + if err != nil { + c.r.Err(err).Msg("cannot open partlist.txt") + gc.JSON(http.StatusInternalServerError, gin.H{"message": "Cannot build avatar."}) + return + } + defer partList.Close() + scanner := bufio.NewScanner(partList) + for scanner.Scan() { + parts = append(parts, scanner.Text()) + } + + // Choose an image for each part + for idx, part := range parts { + // Choose a file for each part + p, _ := fs.Glob(avatarParts, fmt.Sprintf("%s_*", part)) + if len(p) == 0 { + c.r.Error().Msgf("missing part %s", part) + gc.JSON(http.StatusInternalServerError, gin.H{"message": "Cannot build avatar."}) + return + } + parts[idx] = p[randSource.Intn(len(p))] + } + + // Compose the images + var img *image.RGBA + for _, part := range parts { + filePart, err := avatarParts.Open(part) + if err != nil { + c.r.Err(err).Msgf("cannot open part %s", part) + gc.JSON(http.StatusInternalServerError, gin.H{"message": "Cannot build avatar."}) + return + } + imgPart, err := png.Decode(filePart) + filePart.Close() + if err != nil { + c.r.Err(err).Msgf("cannot decode part %s", part) + gc.JSON(http.StatusInternalServerError, gin.H{"message": "Cannot build avatar."}) + return + } + if img == nil { + img = image.NewRGBA(imgPart.Bounds()) + } + draw.Draw(img, img.Bounds(), imgPart, imgPart.Bounds().Min, draw.Over) + } + + // Serve the result + gc.Header("Content-Type", "image/png") + gc.Header("Cache-Control", "max-age=86400") + gc.Header("Vary", "X-Akvorado-User-Login") + gc.Status(http.StatusOK) + png.Encode(gc.Writer, img) +} diff --git a/console/user_test.go b/console/user_test.go new file mode 100644 index 00000000..074368fa --- /dev/null +++ b/console/user_test.go @@ -0,0 +1,97 @@ +package console + +import ( + netHTTP "net/http" + "testing" + + "akvorado/common/clickhousedb" + "akvorado/common/daemon" + "akvorado/common/helpers" + "akvorado/common/http" + "akvorado/common/reporter" + + "github.com/gin-gonic/gin" +) + +func TestUserHandler(t *testing.T) { + r := reporter.NewMock(t) + ch, _ := clickhousedb.NewMock(t, r) + h := http.NewMock(t, r) + c, err := New(r, Configuration{}, Dependencies{ + Daemon: daemon.NewMock(t), + HTTP: h, + ClickHouseDB: ch, + }) + if err != nil { + t.Fatalf("New() error:\n%+v", err) + } + helpers.StartStop(t, c) + + helpers.TestHTTPEndpoints(t, h.Address, helpers.HTTPEndpointCases{ + { + Description: "user info, no user logged in", + URL: "/api/v0/console/user/info", + StatusCode: 401, + JSONOutput: gin.H{"message": "No user logged in."}, + }, { + Description: "user info, minimal user logged in", + URL: "/api/v0/console/user/info", + Header: func() netHTTP.Header { + headers := make(netHTTP.Header) + headers.Add("X-Akvorado-User-Login", "alfred") + return headers + }(), + StatusCode: 200, + JSONOutput: gin.H{ + "login": "alfred", + "avatar-url": "/api/v0/console/user/avatar", + }, + }, { + Description: "user info, complete user logged in", + URL: "/api/v0/console/user/info", + Header: func() netHTTP.Header { + headers := make(netHTTP.Header) + headers.Add("X-Akvorado-User-Login", "alfred") + headers.Add("X-Akvorado-User-Name", "Alfred Pennyworth") + headers.Add("X-Akvorado-User-Email", "alfred@batman.com") + headers.Add("X-Akvorado-User-Avatar", "https://some.example.com/avatar.png") + headers.Add("X-Akvorado-User-Logout", "/logout") + return headers + }(), + StatusCode: 200, + JSONOutput: gin.H{ + "login": "alfred", + "name": "Alfred Pennyworth", + "email": "alfred@batman.com", + "avatar-url": "https://some.example.com/avatar.png", + "logout-url": "/logout", + }, + }, { + Description: "user info, invalid user logged in", + URL: "/api/v0/console/user/info", + Header: func() netHTTP.Header { + headers := make(netHTTP.Header) + headers.Add("X-Akvorado-User-Login", "alfred") + headers.Add("X-Akvorado-User-Email", "alfrednooo") + return headers + }(), + StatusCode: 401, + JSONOutput: gin.H{"message": "No user logged in."}, + }, { + Description: "avatar, no user logged in", + URL: "/api/v0/console/user/avatar", + StatusCode: 401, + JSONOutput: gin.H{"message": "No user logged in."}, + }, { + Description: "avatar, simple user", + URL: "/api/v0/console/user/avatar", + Header: func() netHTTP.Header { + headers := make(netHTTP.Header) + headers.Add("X-Akvorado-User-Login", "alfred") + return headers + }(), + ContentType: "image/png", + StatusCode: 200, + }, + }) +} diff --git a/docker-compose.yml b/docker-compose.yml index 35d9e319..3f53ccba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,3 +29,39 @@ services: ports: - 127.0.0.1:8123:8123/tcp - 127.0.0.1:9000:9000/tcp + + # Authentik + authentik: + image: &authentik-image ghcr.io/goauthentik/server:2022.5.3 + command: server + environment: &authentik-env + AUTHENTIK_REDIS__HOST: authentik-redis + AUTHENTIK_POSTGRESQL__HOST: authentik-postgresql + AUTHENTIK_POSTGRESQL__USER: authentik + AUTHENTIK_POSTGRESQL__NAME: authentik + AUTHENTIK_POSTGRESQL__PASSWORD: dummypassword + AUTHENTIK_DISABLE_UPDATE_CHECK: "true" + AUTHENTIK_ERROR_REPORTING__ENABLED: "false" + AUTHENTIK_DISABLE_STARTUP_ANALYTICS: "true" + AUTHENTIK_AVATARS: none + AUTHENTIK_SECRET_KEY: dummykey + AK_ADMIN_PASS: akadmin + AK_ADMIN_TOKEN: aktoken + ports: + - 127.0.0.1:8999:9000/tcp + depends_on: + - authentik-postgresql + - authentik-redis + - authentik-worker + authentik-postgresql: + image: postgres:12-alpine + environment: + - POSTGRES_PASSWORD=dummypassword + - POSTGRES_USER=authentik + - POSTGRES_DB=authentik + authentik-redis: + image: redis:alpine + authentik-worker: + image: *authentik-image + command: worker + environment: *authentik-env