Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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 | |

Expand Down Expand Up @@ -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:
Expand Down
44 changes: 43 additions & 1 deletion internal/glance/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"log"
mathrand "math/rand/v2"
"net/http"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)

Expand Down
17 changes: 9 additions & 8 deletions internal/glance/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
46 changes: 38 additions & 8 deletions internal/glance/glance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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)

Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions internal/glance/static/css/mobile.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions internal/glance/static/css/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion internal/glance/templates/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{{ end }}

{{ define "navigation-links" }}
{{ range .App.Config.Pages }}
{{ range $.AccessiblePages }}
<a href="{{ $.App.Config.Server.BaseURL }}/{{ .Slug }}" class="nav-item{{ if eq .Slug $.Page.Slug }} nav-item-current{{ end }}"{{ if eq .Slug $.Page.Slug }} aria-current="page"{{ end }}><div class="nav-item-text">{{ .Title }}</div></a>
{{ end }}
{{ end }}
Expand Down Expand Up @@ -44,6 +44,7 @@
</div>
{{ end }}
{{- if .App.RequiresAuth }}
<div class="username-display self-center">{{ .Request.Username }}</div>
<a class="block self-center" href="{{ .App.Config.Server.BaseURL }}/logout" title="Logout">
<svg class="logout-button" stroke="var(--color-text-subdue)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" />
Expand Down Expand Up @@ -93,6 +94,10 @@
{{ end }}

{{ if .App.RequiresAuth }}
<div class="flex justify-between items-center mobile-username-container">
<div class="size-h3">Logged in as:</div>
<div class="mobile-username">{{ .Request.Username }}</div>
</div>
<a href="{{ .App.Config.Server.BaseURL }}/logout" class="flex justify-between items-center">
<div class="size-h3">Logout</div>
<svg class="ui-icon" stroke="var(--color-text-subdue)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
Expand Down