Skip to content

Commit 872b5a0

Browse files
committed
feat: add explicit public access setting
1 parent e67ec79 commit 872b5a0

40 files changed

Lines changed: 857 additions & 74 deletions

docs/public-access.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Public Access
2+
3+
Status: Implemented
4+
5+
## Summary
6+
7+
Whether anonymous visitors may browse an instance is controlled by an explicit administrator setting, not by the canonical instance URL. A fresh or unconfigured instance is private by default.
8+
9+
## Behavior
10+
11+
When public access is **disabled** (the default):
12+
13+
- Anonymous visitors are redirected to the sign-in page in the web UI.
14+
- Anonymous API requests are limited to setup, sign-in, sign-up, identity-provider discovery, and share-token routes.
15+
- Attachments and avatars require authentication.
16+
- RSS feeds (`/explore/rss.xml`, `/u/:username/rss.xml`) return `404`.
17+
18+
When public access is **enabled**:
19+
20+
- Anonymous visitors may browse public memos in Explore and public user profiles.
21+
- Attachments linked from public memos and user avatars are served without authentication.
22+
- RSS feeds for public memos are available.
23+
24+
Authenticated users are never restricted by this setting. Memo visibility (public, protected, private) continues to be enforced independently. Share-token routes keep working on private instances.
25+
26+
## Configuration
27+
28+
Toggle **Settings > System > Access > Allow public access** as an administrator. The change takes effect immediately after saving; no restart is required.
29+
30+
Deployment-managed instances (a `memos-instance-setting-GENERAL.json` file in `/etc/secrets`) reject changes to the whole GENERAL setting with a failed-precondition error, as with the other options on that page.
31+
32+
## Relationship to `MEMOS_INSTANCE_URL`
33+
34+
`--instance-url` / `MEMOS_INSTANCE_URL` remains the canonical URL used for link generation, OAuth redirect URLs, RSS enclosure URLs, and deployment metadata. It no longer influences access control. You can:
35+
36+
- Keep the instance private while still configuring a canonical URL.
37+
- Allow public access without configuring a canonical URL, although a canonical URL is recommended so generated links are stable.
38+
39+
## Upgrade note
40+
41+
Earlier releases treated a non-empty instance URL as permission for anonymous access. After upgrading, such instances remain configured with the URL but become private until an administrator enables **Allow public access**.

internal/profile/profile.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"path/filepath"
88
"runtime"
99
"strings"
10+
"sync/atomic"
1011

1112
"github.com/pkg/errors"
1213
)
@@ -32,19 +33,35 @@ type Profile struct {
3233
Version string
3334
// Commit is the current build commit of server
3435
Commit string
35-
// InstanceURL is the url of your memos instance.
36+
// InstanceURL is the url of your memos instance. It is used only for
37+
// canonical URL and link generation; it never controls anonymous access.
3638
InstanceURL string
39+
40+
// allowAnonymous mirrors the persisted GENERAL instance setting
41+
// (allow_public_access). The zero value is false, so a fresh or
42+
// unconfigured instance is private.
43+
allowAnonymous atomic.Bool
3744
}
3845

39-
// AllowAnonymous reports whether unauthenticated visitors may access the instance.
40-
//
41-
// Anonymous access is enabled only when an InstanceURL is configured. An instance
42-
// with no InstanceURL set is treated as private: anonymous callers are limited to
43-
// the auth-bootstrap endpoints (sign-in, share links, etc.) and the web UI redirects
44-
// them to the sign-in page instead of the public Explore view. Authenticated callers
45-
// (session, access token, or personal access token) are never affected.
46+
// SetAllowAnonymous publishes the effective anonymous-access policy. It is
47+
// called by the store whenever the GENERAL instance setting is read or updated,
48+
// so an administrator toggle takes effect immediately without a restart.
49+
func (p *Profile) SetAllowAnonymous(allowed bool) {
50+
if p == nil {
51+
return
52+
}
53+
p.allowAnonymous.Store(allowed)
54+
}
55+
56+
// AllowAnonymous reports whether unauthenticated visitors may access the
57+
// instance, per the persisted allow_public_access setting. Absent or unloaded
58+
// state is private. Authenticated callers (session, access token, or personal
59+
// access token) are never affected by this policy.
4660
func (p *Profile) AllowAnonymous() bool {
47-
return strings.TrimSpace(p.InstanceURL) != ""
61+
if p == nil {
62+
return false
63+
}
64+
return p.allowAnonymous.Load()
4865
}
4966

5067
func checkDataDir(dataDir string) (string, error) {

internal/profile/profile_test.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ func TestAllowAnonymous(t *testing.T) {
88
url string
99
want bool
1010
}{
11-
{"empty is private", "", false},
12-
{"whitespace only is private", " ", false},
13-
{"configured url is public", "https://memos.example.com", true},
14-
{"configured url with padding is public", " https://memos.example.com ", true},
11+
{"empty url stays private", "", false},
12+
{"configured url alone is private", "https://memos.example.com", false},
1513
}
1614
for _, c := range cases {
1715
t.Run(c.name, func(t *testing.T) {
@@ -22,3 +20,18 @@ func TestAllowAnonymous(t *testing.T) {
2220
})
2321
}
2422
}
23+
24+
func TestAllowAnonymousExplicitPolicy(t *testing.T) {
25+
p := &Profile{}
26+
if p.AllowAnonymous() {
27+
t.Fatal("fresh profile must default to private")
28+
}
29+
p.SetAllowAnonymous(true)
30+
if !p.AllowAnonymous() {
31+
t.Fatal("explicit public policy must be reported")
32+
}
33+
p.SetAllowAnonymous(false)
34+
if p.AllowAnonymous() {
35+
t.Fatal("explicit private policy must be reported")
36+
}
37+
}

proto/api/v1/instance_service.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ message InstanceProfile {
8181
// false once any user exists, so an instance that has lost its admins is not
8282
// mistaken for a fresh install.
8383
bool needs_setup = 9;
84+
85+
// Whether anonymous visitors may access public instance content.
86+
// Absent or false means the instance is private.
87+
bool allow_public_access = 10;
8488
}
8589

8690
// Request for instance profile.
@@ -146,6 +150,9 @@ message InstanceSetting {
146150
bool disallow_change_username = 8;
147151
// disallow_change_nickname disallows changing nickname.
148152
bool disallow_change_nickname = 9;
153+
// allow_public_access controls whether anonymous visitors may access public
154+
// instance content. Absent or false means the instance is private.
155+
bool allow_public_access = 10;
149156

150157
// Custom profile configuration for instance branding.
151158
message CustomProfile {

proto/gen/api/v1/instance_service.pb.go

Lines changed: 34 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/gen/openapi.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2790,6 +2790,11 @@ components:
27902790
setup (creating the first admin account). Unlike a null admin, this stays
27912791
false once any user exists, so an instance that has lost its admins is not
27922792
mistaken for a fresh install.
2793+
allowPublicAccess:
2794+
type: boolean
2795+
description: |-
2796+
Whether anonymous visitors may access public instance content.
2797+
Absent or false means the instance is private.
27932798
description: Instance profile message containing basic instance information.
27942799
InstanceSetting:
27952800
type: object
@@ -2888,6 +2893,11 @@ components:
28882893
disallowChangeNickname:
28892894
type: boolean
28902895
description: disallow_change_nickname disallows changing nickname.
2896+
allowPublicAccess:
2897+
type: boolean
2898+
description: |-
2899+
allow_public_access controls whether anonymous visitors may access public
2900+
instance content. Absent or false means the instance is private.
28912901
description: General instance settings configuration.
28922902
InstanceSetting_MemoRelatedSetting:
28932903
type: object

proto/gen/store/instance_setting.pb.go

Lines changed: 16 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/store/instance_setting.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ message InstanceGeneralSetting {
6363
bool disallow_change_username = 8;
6464
// disallow_change_nickname disallows changing nickname.
6565
bool disallow_change_nickname = 9;
66+
// allow_public_access controls whether anonymous visitors may access public
67+
// instance content. Absent or false means the instance is private.
68+
bool allow_public_access = 10;
6669
}
6770

6871
message InstanceCustomProfile {

server/router/api/v1/acl_config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func IsPublicMethod(procedure string) bool {
5252
}
5353

5454
// AuthBootstrapMethods is the subset of PublicMethods that stays reachable by
55-
// anonymous callers even when the instance is private (no InstanceURL configured).
55+
// anonymous callers even when the instance is private (public access disabled).
5656
//
5757
// It is the minimum required to render the sign-in page, authenticate, and follow
5858
// share links, and register when instance settings permit it. Every entry here

server/router/api/v1/acl_config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func TestAuthBootstrapMethodsAreSubsetOfPublic(t *testing.T) {
106106
}
107107

108108
// TestAuthBootstrapClassification verifies which endpoints remain reachable by
109-
// anonymous callers on a private instance (no InstanceURL configured).
109+
// anonymous callers on a private instance (public access disabled).
110110
func TestAuthBootstrapClassification(t *testing.T) {
111111
// Reachable while private: sign-in flow, registration, instance metadata, SSO, share links.
112112
bootstrap := []string{

0 commit comments

Comments
 (0)