feat: 统一 Commilitia Drop 全客户端命名与分发

This commit is contained in:
2026-07-31 22:56:10 +08:00
parent f7b0f04c9c
commit c480f0d1c2
77 changed files with 861 additions and 263 deletions
+5 -5
View File
@@ -44,7 +44,7 @@ type Config struct {
// 内网直连(绝不经公网反代,以保 /internal/* 与 /refresh 可达)。
// BrokerInternalKey —— broker /internal/* 端点的共享密钥(作 X-Internal-Key 发送),
// prod 必填。
// BrokerApp —— 本应用在 broker apps 注册表里的 key(默认 "cdrop")。
// BrokerApp —— 本应用在 broker apps 注册表里的 key(默认 "commilitia-drop")。
BrokerBaseURL string `koanf:"broker_base_url"`
BrokerInternalKey string `koanf:"broker_internal_key"`
BrokerApp string `koanf:"broker_app"`
@@ -96,7 +96,7 @@ type Config struct {
// APNSKeyPath — filesystem path to the .p8 (PEM PKCS8 ES256) key Apple issues.
// APNSKeyID — the 10-character Key ID from the Apple Developer portal.
// APNSTeamID — the 10-character Team ID from the Apple Developer portal.
// APNSTopic — the app bundle ID (e.g. "net.commilitia.cdrop").
// APNSTopic — the app bundle ID (e.g. "net.commilitia.Commilitia-Drop").
// APNSEnv — "prod" (default) or "sandbox" (dev/TestFlight builds).
// When unset, APNs is disabled and the register endpoint returns 503.
APNSKeyPath string `koanf:"apns_key_path"`
@@ -123,12 +123,12 @@ func (c *Config) QRLoginOn() bool {
return c.QRLoginEnabled && c.BrokerBaseURL != ""
}
// BrokerAppOrDefault returns the configured broker app key, defaulting to "cdrop".
// BrokerAppOrDefault returns the configured broker app key, defaulting to "commilitia-drop".
func (c *Config) BrokerAppOrDefault() string {
if c.BrokerApp != "" {
return c.BrokerApp
}
return "cdrop"
return "commilitia-drop"
}
// Load reads config from optional ./config.yaml then overrides with CDROP_* env.
@@ -144,7 +144,7 @@ func Load() (*Config, error) {
k.Set("clipboard_debounce_sec", 3)
k.Set("qr_login_enabled", true)
k.Set("qr_request_ttl_seconds", 120)
k.Set("broker_app", "cdrop")
k.Set("broker_app", "commilitia-drop")
k.Set("full_access_ttl_seconds", 900)
k.Set("full_refresh_ttl_seconds", 604800)
k.Set("guest_access_ttl_seconds", 900)
+11 -11
View File
@@ -83,7 +83,7 @@ func newMockBroker(t *testing.T) (*brokerclient.Client, *mockBrokerState) {
}
st.sessions[sid] = &mockSession{sid: sid, userID: userID, app: app, meta: meta, sub: str(body, "sub"), label: label, scope: scope, createdAt: created, lastUsedAt: now}
_ = json.NewEncoder(w).Encode(map[string]any{
"id": sid, "app": "cdrop",
"id": sid, "app": "commilitia-drop",
"access": "acc-" + sid, "refresh": "rtk-" + sid,
"access_expires": time.Now().Add(15 * time.Minute).Unix(),
"refresh_expires": time.Now().Add(24 * time.Hour).Unix(),
@@ -128,7 +128,7 @@ func newMockBroker(t *testing.T) (*brokerclient.Client, *mockBrokerState) {
}
}))
t.Cleanup(srv.Close)
return brokerclient.New(srv.URL, "test-key", "cdrop"), st
return brokerclient.New(srv.URL, "test-key", "commilitia-drop"), st
}
// newQRTestServer builds a Server with only the fields the scan-login + device
@@ -332,10 +332,10 @@ func TestRequireFullSession_GuestBlockedFullPasses(t *testing.T) {
handler.ServeHTTP(w, r)
return w.Code
}
if c := check("app:cdrop:guest"); c != http.StatusForbidden {
if c := check("app:commilitia-drop:guest"); c != http.StatusForbidden {
t.Errorf("guest on full-only route: got %d, want 403", c)
}
if c := check("app:cdrop:full"); c != http.StatusOK {
if c := check("app:commilitia-drop:full"); c != http.StatusOK {
t.Errorf("full on full-only route: got %d, want 200", c)
}
}
@@ -365,8 +365,8 @@ func TestDeleteDevice_RevokesBrokerSession(t *testing.T) {
if !st.revoked["sid-1"] {
t.Error("broker session sid-1 was not revoked")
}
if st.lastRevokeApp != "cdrop" {
t.Errorf("revoke X-Broker-App: got %q, want cdrop", st.lastRevokeApp)
if st.lastRevokeApp != "commilitia-drop" {
t.Errorf("revoke X-Broker-App: got %q, want commilitia-drop", st.lastRevokeApp)
}
if _, err := s.queries.GetDevice(context.Background(), got.DeviceID); err == nil {
t.Error("device row should be gone after delete")
@@ -381,7 +381,7 @@ func TestSessionsList_ShowsDevices(t *testing.T) {
got := decodeStatus(t, qrStatus(s, start.RequestID, start.PollSecret))
r := httptest.NewRequest(http.MethodGet, "/api/auth/sessions", nil)
r = r.WithContext(jwtauth.ContextWithClaims(r.Context(), &jwtauth.Claims{UserID: "owner", Scope: "app:cdrop:guest", DeviceID: got.DeviceID}))
r = r.WithContext(jwtauth.ContextWithClaims(r.Context(), &jwtauth.Claims{UserID: "owner", Scope: "app:commilitia-drop:guest", DeviceID: got.DeviceID}))
w := httptest.NewRecorder()
s.handleSessionsList(w, r)
if w.Code != http.StatusOK {
@@ -462,7 +462,7 @@ func TestDeviceSession_MintsManagedDevice(t *testing.T) {
if st.lastMint["tier"] != "full" || st.lastMint["meta"] != "dev_browser01" || st.lastMint["label"] != "Laptop" {
t.Errorf("mint params: %+v", st.lastMint)
}
list := listSessions(t, s, "owner", "app:cdrop:full", "dev_browser01")
list := listSessions(t, s, "owner", "app:commilitia-drop:full", "dev_browser01")
if len(list) != 1 || list[0].DeviceID != "dev_browser01" || list[0].Kind != "browser" || !list[0].Current {
t.Errorf("unified list wrong: %+v", list)
}
@@ -474,7 +474,7 @@ func TestDeviceSession_Idempotent(t *testing.T) {
s, _ := newQRTestServer(t)
_ = deviceSession(t, s, "owner", "full", "dev_same01", "Laptop", "browser", "")
_ = deviceSession(t, s, "owner", "full", "dev_same01", "Laptop Renamed", "browser", "")
list := listSessions(t, s, "owner", "app:cdrop:full", "dev_same01")
list := listSessions(t, s, "owner", "app:commilitia-drop:full", "dev_same01")
if len(list) != 1 {
t.Fatalf("idempotent re-mint: got %d sessions, want 1", len(list))
}
@@ -493,7 +493,7 @@ func TestSessionsList_FiltersMetalessBootstrap(t *testing.T) {
t.Fatalf("bootstrap mint: %v", err)
}
_ = deviceSession(t, s, "owner", "full", "dev_real01", "Laptop", "browser", "")
list := listSessions(t, s, "owner", "app:cdrop:full", "dev_real01")
list := listSessions(t, s, "owner", "app:commilitia-drop:full", "dev_real01")
if len(list) != 1 || list[0].DeviceID != "dev_real01" {
t.Fatalf("metaless bootstrap not filtered: %+v", list)
}
@@ -517,7 +517,7 @@ func TestDeviceSession_RejectsCrossOrigin(t *testing.T) {
// A restricted guest minting its device session stays guest — no escalation to full.
func TestDeviceSession_GuestTierNotEscalated(t *testing.T) {
s, st := newQRTestServer(t)
_ = deviceSession(t, s, "owner", "app:cdrop:guest", "dev_guest01", "Borrowed", "browser", "")
_ = deviceSession(t, s, "owner", "app:commilitia-drop:guest", "dev_guest01", "Borrowed", "browser", "")
if st.lastMint["tier"] != "guest" {
t.Errorf("guest caller minted tier %v, want guest", st.lastMint["tier"])
}
+2 -2
View File
@@ -15,7 +15,7 @@ type Claims struct {
Avatar string // X-Auth-Avatar (profile picture URL from the broker account); may be empty
Groups []string // X-Auth-Roles, comma-split
// Scope is the raw X-Auth-Scope: a global SSO user is "full"; a cdrop delegated
// session is "app:cdrop:<tier>". Tier() reads the capability grade off the end.
// session is "app:commilitia-drop:<tier>". Tier() reads the capability grade off the end.
Scope string
// DeviceID is X-Auth-Meta: the cdrop device_id this session was minted for — the
// join key to the devices row. Empty for an unmanaged caller (e.g. a global SSO
@@ -24,7 +24,7 @@ type Claims struct {
}
// ScopeTier returns the capability grade — the last colon-separated segment of a broker
// scope ("app:cdrop:guest" → "guest", "full" → "full"). A tierless scope is its own tier.
// scope ("app:commilitia-drop:guest" → "guest", "full" → "full"). A tierless scope is its own tier.
// Shared by Claims.Tier() and the session-list overlay so the two never diverge.
func ScopeTier(scope string) string {
if i := strings.LastIndex(scope, ":"); i >= 0 {
+1 -1
View File
@@ -150,7 +150,7 @@ func bearerToken(r *http.Request) (string, bool) {
func unauthorized(w http.ResponseWriter, reason string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("WWW-Authenticate", `Bearer realm="cdrop"`)
w.Header().Set("WWW-Authenticate", `Bearer realm="Commilitia Drop"`)
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(map[string]string{
"error": "unauthorized",
+6 -6
View File
@@ -38,7 +38,7 @@ func TestMiddleware_ProdReadsAuthHeaders(t *testing.T) {
a := New(&config.Config{AuthMode: "prod"}, &fakeDeviceStore{})
r := httptest.NewRequest(http.MethodGet, "/api/me", nil)
r.Header.Set("X-Auth-Subject", "user-1")
r.Header.Set("X-Auth-Scope", "app:cdrop:guest")
r.Header.Set("X-Auth-Scope", "app:commilitia-drop:guest")
r.Header.Set("X-Auth-Meta", "dev_abc")
r.Header.Set("X-Auth-Name", "Alice")
r.Header.Set("X-Auth-Roles", "admin, user")
@@ -54,7 +54,7 @@ func TestMiddleware_ProdReadsAuthHeaders(t *testing.T) {
t.Errorf("claims wrong: %+v", c)
}
if !c.Guest() {
t.Error("app:cdrop:guest scope should mark Guest()")
t.Error("app:commilitia-drop:guest scope should mark Guest()")
}
if len(c.Groups) != 2 || c.Groups[0] != "admin" || c.Groups[1] != "user" {
t.Errorf("groups: got %v", c.Groups)
@@ -78,7 +78,7 @@ func TestMiddleware_TouchesManagedDevice(t *testing.T) {
a := New(&config.Config{AuthMode: "prod"}, fs)
r := httptest.NewRequest(http.MethodGet, "/api/me", nil)
r.Header.Set("X-Auth-Subject", "user-1")
r.Header.Set("X-Auth-Scope", "app:cdrop:full")
r.Header.Set("X-Auth-Scope", "app:commilitia-drop:full")
r.Header.Set("X-Auth-Meta", "dev_x")
runMiddleware(a, r)
if len(fs.touched) != 1 {
@@ -133,10 +133,10 @@ func TestClaimsTier(t *testing.T) {
tier string
guest bool
}{
{"app:cdrop:guest", "guest", true},
{"app:cdrop:full", "full", false},
{"app:commilitia-drop:guest", "guest", true},
{"app:commilitia-drop:full", "full", false},
{"full", "full", false},
{"app:cdrop", "cdrop", false},
{"app:commilitia-drop", "commilitia-drop", false},
{"", "", false},
}
for _, tc := range cases {