From cf3b39639835d6aaa0be6ce4449c279f09d3d1af Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Thu, 27 Aug 2026 22:00:02 +0200 Subject: [PATCH] Document consumer README skeleton and add package Examples. Co-authored-by: Cursor --- README.md | 12 +++++-- access_gate.go | 4 +++ example_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 example_test.go diff --git a/README.md b/README.md index 2fc0445..d5c2cfe 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # go-nc-exapp -Shared Go library for Nextcloud ExApp Services: AppAPI authentication, OCS JSON calls, per-user ExApp preferences, and optional Required Groups Access Gate. +Shared Go library for Nextcloud ExApp Services: AppAPI authentication, OCS JSON calls, per-user ExApp preferences, and an optional Required Groups Access Gate. -Import: `gitea.neitzel.de/konrad/go-nc-exapp` +Import: `gitea.neitzel.de/konrad/go-nc-exapp` (package `gonexapp`). ## Scope @@ -14,7 +14,7 @@ Import: `gitea.neitzel.de/konrad/go-nc-exapp` - **UserFromRequest** — extract the requesting user from inbound AppAPI-proxied requests - **OCSClient** — authenticated OCS calls that always append `format=json` - **AppAPIPreferences** — parameterized get/set of a string ExApp preference (caller supplies app id and key) -- **Access Gate** — optional Required Groups enforcement (`Wrap` + `Check`), English denied HTML, positive membership cache; env helpers for `REQUIRED_GROUPS` / `REQUIRED_GROUPS_CACHE_SECONDS` +- **Access Gate** — optional Required Groups enforcement (`Wrap` + `Check`), English denied HTML for browsers, positive membership cache; env helpers for `REQUIRED_GROUPS` / `REQUIRED_GROUPS_CACHE_SECONDS` **Excluded** @@ -49,10 +49,16 @@ Each ExApp chooses its own preference keys; this library does not hardcode produ Declare `REQUIRED_GROUPS` and `REQUIRED_GROUPS_CACHE_SECONDS` in the ExApp `info.xml` so Deploy options can set them. +Runnable package examples: `go test -run Example`. + ## Domain language See [CONTEXT.md](./CONTEXT.md) for AppAPI credentials, Requesting user, ExApp preference, OCS, Required Groups, and Access Gate terminology. +## Testing + +Unit tests use `httptest` fake OCS servers. No live Nextcloud is required for Library CI. + ## Related - **go-nc-files** — WebDAV, Working Folder, Saved Default, Visit resolution diff --git a/access_gate.go b/access_gate.go index 5fbd680..97ba5a0 100644 --- a/access_gate.go +++ b/access_gate.go @@ -32,9 +32,13 @@ type cacheEntry struct { type CheckResult int const ( + // CheckAllowed means the request may proceed (or the gate is inactive / skipped). CheckAllowed CheckResult = iota + // CheckDenied means the Requesting user is not in Required Groups. CheckDenied + // CheckUnauthorized means no Requesting user could be read from the request. CheckUnauthorized + // CheckUnavailable means group membership could not be determined (e.g. OCS error). CheckUnavailable ) diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..92b76ef --- /dev/null +++ b/example_test.go @@ -0,0 +1,89 @@ +package gonexapp_test + +import ( + "encoding/base64" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + + gonexapp "gitea.neitzel.de/konrad/go-nc-exapp" +) + +func ExampleUserFromRequest() { + token := base64.StdEncoding.EncodeToString([]byte("alice:secret")) + req := httptest.NewRequest(http.MethodGet, "/api", nil) + req.Header.Set("AUTHORIZATION-APP-API", token) + + user, err := gonexapp.UserFromRequest(req) + if err != nil { + fmt.Println("err:", err) + return + } + fmt.Println(user) + // Output: alice +} + +func ExampleCredentials_AuthHeaders() { + cred := gonexapp.Credentials{ + BaseURL: "https://nextcloud.example", AppID: "myexapp", AppVersion: "0.1.0", + AAVersion: "1.0.0", AppSecret: "s", UserID: "alice", + } + h := cred.AuthHeaders() + fmt.Println(h.Get("EX-APP-ID"), h.Get("OCS-APIRequest") != "") + // Output: myexapp true +} + +func ExampleNewAppAPIPreferences() { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "get-values") { + _, _ = io.WriteString(w, `{"ocs":{"data":[{"configkey":"savedDefault","configvalue":"Zones"}]}}`) + return + } + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, `{"ocs":{"data":{}}}`) + })) + defer srv.Close() + + cred := gonexapp.Credentials{ + BaseURL: srv.URL, AppID: "myexapp", AppVersion: "0.1.0", AAVersion: "1.0.0", + AppSecret: "s", UserID: "alice", + } + prefs := gonexapp.NewAppAPIPreferences(cred, "myexapp", "savedDefault") + prefs.Client = srv.Client() + prefs.OCS.Client = srv.Client() + + value, err := prefs.Get() + if err != nil { + fmt.Println("err:", err) + return + } + if err := prefs.Set("Zones"); err != nil { + fmt.Println("set:", err) + return + } + fmt.Println(value) + // Output: Zones +} + +func ExampleAccessGate_Wrap() { + inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = io.WriteString(w, "ok") + }) + // Empty Required Groups: gate is inactive. + h := gonexapp.AccessGate{}.Wrap(inner) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api", nil)) + fmt.Println(rec.Code, rec.Body.String()) + // Output: 200 ok +} + +func ExampleResolveRequiredGroups() { + fmt.Println(gonexapp.ResolveRequiredGroups("", false, nil)) + fmt.Println(len(gonexapp.ResolveRequiredGroups("", true, []string{"ops"}))) + // Output: + // [] + // 0 +}