From f406e27eaa2662be920930eff99fe4fb5b620fce Mon Sep 17 00:00:00 2001 From: Abhi Singh Date: Mon, 3 Nov 2025 00:00:59 +0530 Subject: [PATCH 1/2] Implement user authentication and access control features - Add functions to retrieve username from request and check page access. - Update page struct to include allowed users. - Modify template data to include accessible pages for the user. - Enhance HTML templates to display logged-in username and restrict access to pages based on user permissions. - Introduce new CSS styles for username display in mobile and site views. --- internal/glance/auth.go | 44 ++++++++++++++++++++++++- internal/glance/config.go | 17 +++++----- internal/glance/glance.go | 46 ++++++++++++++++++++++----- internal/glance/static/css/mobile.css | 10 ++++++ internal/glance/static/css/site.css | 7 ++++ internal/glance/templates/page.html | 7 +++- 6 files changed, 113 insertions(+), 18 deletions(-) diff --git a/internal/glance/auth.go b/internal/glance/auth.go index e6497a19..ba063a0d 100644 --- a/internal/glance/auth.go +++ b/internal/glance/auth.go @@ -13,6 +13,7 @@ import ( "log" mathrand "math/rand/v2" "net/http" + "slices" "strconv" "strings" "time" @@ -285,6 +286,46 @@ func (a *application) isAuthorized(w http.ResponseWriter, r *http.Request) bool return true } +func (a *application) getUsernameFromRequest(r *http.Request) string { + if !a.RequiresAuth { + return "" + } + + token, err := r.Cookie(AUTH_SESSION_COOKIE_NAME) + if err != nil || token.Value == "" { + return "" + } + + usernameHash, _, err := verifySessionToken(token.Value, a.authSecretKey, time.Now()) + if err != nil { + return "" + } + + username, exists := a.usernameHashToUsername[string(usernameHash)] + if !exists { + return "" + } + + _, exists = a.Config.Auth.Users[username] + if !exists { + return "" + } + + return username +} + +func (a *application) userHasPageAccess(username string, page *page) bool { + if !a.RequiresAuth { + return true + } + + if len(page.AllowedUsers) == 0 { + return true + } + + return slices.Contains(page.AllowedUsers, username) +} + // Handles sending the appropriate response for an unauthorized request and returns true if the request was unauthorized func (a *application) handleUnauthorizedResponse(w http.ResponseWriter, r *http.Request, fallback doWhenUnauthorized) bool { if a.isAuthorized(w, r) { @@ -327,7 +368,8 @@ func (a *application) handleLoginPageRequest(w http.ResponseWriter, r *http.Requ } data := &templateData{ - App: a, + App: a, + AccessiblePages: nil, } a.populateTemplateRequestData(&data.Request, r) diff --git a/internal/glance/config.go b/internal/glance/config.go index d4d6af0e..312feefc 100644 --- a/internal/glance/config.go +++ b/internal/glance/config.go @@ -75,14 +75,15 @@ type user struct { } type page struct { - Title string `yaml:"name"` - Slug string `yaml:"slug"` - Width string `yaml:"width"` - DesktopNavigationWidth string `yaml:"desktop-navigation-width"` - ShowMobileHeader bool `yaml:"show-mobile-header"` - HideDesktopNavigation bool `yaml:"hide-desktop-navigation"` - CenterVertically bool `yaml:"center-vertically"` - HeadWidgets widgets `yaml:"head-widgets"` + Title string `yaml:"name"` + Slug string `yaml:"slug"` + Width string `yaml:"width"` + DesktopNavigationWidth string `yaml:"desktop-navigation-width"` + ShowMobileHeader bool `yaml:"show-mobile-header"` + HideDesktopNavigation bool `yaml:"hide-desktop-navigation"` + CenterVertically bool `yaml:"center-vertically"` + AllowedUsers []string `yaml:"allowed-users"` + HeadWidgets widgets `yaml:"head-widgets"` Columns []struct { Size string `yaml:"size"` Widgets widgets `yaml:"widgets"` diff --git a/internal/glance/glance.go b/internal/glance/glance.go index cc961572..9da199ee 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -221,7 +221,7 @@ func newApplication(c *config) (*application, error) { config.Branding.AppBackgroundColor = config.Theme.BackgroundColorAsHex } - manifest, err := executeTemplateToString(manifestTemplate, templateData{App: app}) + manifest, err := executeTemplateToString(manifestTemplate, templateData{App: app, AccessiblePages: nil}) if err != nil { return nil, fmt.Errorf("parsing manifest.json: %v", err) } @@ -278,13 +278,26 @@ func (a *application) resolveUserDefinedAssetPath(path string) string { } type templateRequestData struct { - Theme *themeProperties + Theme *themeProperties + Username string } type templateData struct { - App *application - Page *page - Request templateRequestData + App *application + Page *page + Request templateRequestData + AccessiblePages []*page +} + +func (a *application) getAccessiblePages(username string) []*page { + accessiblePages := make([]*page, 0, len(a.Config.Pages)) + for i := range a.Config.Pages { + p := &a.Config.Pages[i] + if a.userHasPageAccess(username, p) { + accessiblePages = append(accessiblePages, p) + } + } + return accessiblePages } func (a *application) populateTemplateRequestData(data *templateRequestData, r *http.Request) { @@ -301,6 +314,7 @@ func (a *application) populateTemplateRequestData(data *templateRequestData, r * } data.Theme = theme + data.Username = a.getUsernameFromRequest(r) } func (a *application) handlePageRequest(w http.ResponseWriter, r *http.Request) { @@ -314,9 +328,16 @@ func (a *application) handlePageRequest(w http.ResponseWriter, r *http.Request) return } + username := a.getUsernameFromRequest(r) + if !a.userHasPageAccess(username, page) { + http.Error(w, "403 Forbidden - You don't have access to this page", http.StatusForbidden) + return + } + data := templateData{ - Page: page, - App: a, + Page: page, + App: a, + AccessiblePages: a.getAccessiblePages(username), } a.populateTemplateRequestData(&data.Request, r) @@ -342,8 +363,17 @@ func (a *application) handlePageContentRequest(w http.ResponseWriter, r *http.Re return } + // Check if user has access to this specific page + username := a.getUsernameFromRequest(r) + if !a.userHasPageAccess(username, page) { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte(`{"error": "Forbidden - You don't have access to this page"}`)) + return + } + pageData := templateData{ - Page: page, + Page: page, + AccessiblePages: nil, } var err error diff --git a/internal/glance/static/css/mobile.css b/internal/glance/static/css/mobile.css index 2b71a247..d5b2f510 100644 --- a/internal/glance/static/css/mobile.css +++ b/internal/glance/static/css/mobile.css @@ -59,6 +59,16 @@ background-color: var(--color-widget-background-highlight); } + .mobile-username-container { + cursor: default; + pointer-events: none; + } + + .mobile-username { + font-weight: 600; + color: var(--color-primary); + } + .mobile-navigation:has(.mobile-navigation-page-links-input:checked) .hamburger-icon { --spacing: 7px; color: var(--color-primary); diff --git a/internal/glance/static/css/site.css b/internal/glance/static/css/site.css index cd308234..f12823c3 100644 --- a/internal/glance/static/css/site.css +++ b/internal/glance/static/css/site.css @@ -325,6 +325,13 @@ kbd:active { color: var(--color-text-highlight); } +.username-display { + margin-right: 1rem; + font-size: var(--font-size-h3); + color: var(--color-primary); + font-weight: 600; +} + .logout-button { width: 2rem; height: 2rem; diff --git a/internal/glance/templates/page.html b/internal/glance/templates/page.html index 45619e9e..aa8a52bc 100644 --- a/internal/glance/templates/page.html +++ b/internal/glance/templates/page.html @@ -7,7 +7,7 @@ {{ end }} {{ define "navigation-links" }} -{{ range .App.Config.Pages }} +{{ range $.AccessiblePages }} {{ end }} {{ end }} @@ -44,6 +44,7 @@ {{ end }} {{- if .App.RequiresAuth }} +
{{ .Request.Username }}
@@ -93,6 +94,10 @@ {{ end }} {{ if .App.RequiresAuth }} +
+
Logged in as:
+
{{ .Request.Username }}
+
Logout
From da94b30b9f9bd3c8ec4739700fa2cef67a64934a Mon Sep 17 00:00:00 2001 From: Abhi Singh Date: Mon, 3 Nov 2025 00:23:28 +0530 Subject: [PATCH 2/2] Add user access control for specific pages in configuration docs --- docs/configuration.md | 54 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index c42b04d9..d74c676f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -283,6 +283,56 @@ server: When set to `true`, Glance will use the `X-Forwarded-For` header to determine the original IP address of the request, so make sure that your reverse proxy is correctly configured to send that header. +### Restricting pages to specific users + +You can restrict access to specific pages by using the `allowed-users` property on a page. When this property is set, only the listed users will be able to access that page. Users not in the list will receive a 403 Forbidden error when attempting to access the page. Example: + +```yaml +auth: + secret-key: # generated secret key + users: + admin: + password: admin123 + john: + password: john123 + jane: + password: jane123 + +pages: + - name: Home + # No allowed-users property means all authenticated users can access this page + columns: + - size: full + widgets: + - type: rss + + - name: Admin + allowed-users: + - admin + columns: + - size: full + widgets: + - type: monitor + + - name: Work + allowed-users: + - john + - jane + columns: + - size: full + widgets: + - type: calendar +``` + +In this example: +- The "Home" page is accessible by all authenticated users (admin, john, and jane) +- The "Admin" page is only accessible by the `admin` user +- The "Work" page is accessible by both `john` and `jane` + +> [!NOTE] +> +> If a page does not have the `allowed-users` property set, all authenticated users will be able to access it. If you want to restrict access to all pages, you must explicitly set the `allowed-users` property on each page. + ## Server Server configuration is done through a top level `server` property. Example: @@ -560,6 +610,7 @@ pages: | center-vertically | boolean | no | false | | hide-desktop-navigation | boolean | no | false | | show-mobile-header | boolean | no | false | +| allowed-users | array | no | | | head-widgets | array | no | | | columns | array | yes | | @@ -598,6 +649,9 @@ Preview: ![](images/mobile-header-preview.png) +#### `allowed-users` +A list of usernames that are allowed to access this page. When this property is set, only the listed users will be able to view the page. Users not in the list will receive a 403 Forbidden error. If this property is not added, all authenticated users will be able to access the page. See the [Restricting pages to specific users](#restricting-pages-to-specific-users) section for more details and examples. + #### `head-widgets` Head widgets will be shown at the top of the page, above the columns, and take up the combined width of all columns. You can specify any widget, though some will look better than others, such as the markets, RSS feed with `horizontal-cards` style, and videos widgets. Example: