diff --git a/README.API b/README.API index d0d379f..abb90ef 100644 --- a/README.API +++ b/README.API @@ -55,6 +55,14 @@ on-disk format but without any user definitions or cryptographic keys. Allowed methods are HEAD, GET, PUT and DELETE. The only accepted content-type is `application/json`. +### Fallback users + + /galene-api/0/.groups/groupname/.fallback-users + +Contains fallback user descriptions, in the same format as the +`fallbackUsers` field of the on-disk format. Allowed methods are PUT and +DELETE. The only accepted content-type is `application/json`. + ### Authentication keys /galene-api/0/.groups/groupname/.keys diff --git a/group/description.go b/group/description.go index 37c43de..81e85bd 100644 --- a/group/description.go +++ b/group/description.go @@ -540,6 +540,18 @@ func GetDescriptionNames() ([]string, error) { return names, err } +func SetFallbackUsers(group string, users []UserDescription) error { + groups.mu.Lock() + defer groups.mu.Unlock() + + desc, err := readDescription(group, false) + if err != nil { + return err + } + desc.FallbackUsers = users + return rewriteDescriptionFile(desc.FileName, desc) +} + func SetKeys(group string, keys []map[string]any) error { groups.mu.Lock() defer groups.mu.Unlock() diff --git a/webserver/api.go b/webserver/api.go index 855a805..d83c773 100644 --- a/webserver/api.go +++ b/webserver/api.go @@ -128,6 +128,9 @@ func apiGroupHandler(w http.ResponseWriter, r *http.Request, pth string) { if kind == ".users" { apiUserHandler(w, r, g, rest) return + } else if kind == ".fallback-users" && rest == "" { + fallbackUsersHandler(w, r, g) + return } else if kind == ".keys" && rest == "" { keysHandler(w, r, g) return @@ -433,6 +436,46 @@ func passwordHandler(w http.ResponseWriter, r *http.Request, g, user string) { return } +func fallbackUsersHandler(w http.ResponseWriter, r *http.Request, g string) { + if !checkAdmin(w, r) { + return + } + + if r.Method == "PUT" { + ctype := parseContentType(r.Header.Get("Content-Type")) + if !strings.EqualFold(ctype, "application/json") { + http.Error(w, "unsupported content type", + http.StatusUnsupportedMediaType) + return + } + d := json.NewDecoder(r.Body) + var users []group.UserDescription + err := d.Decode(&users) + if err != nil { + httpError(w, err) + return + } + err = group.SetFallbackUsers(g, users) + if err != nil { + httpError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) + return + } else if r.Method == "DELETE" { + err := group.SetFallbackUsers(g, nil) + if err != nil { + httpError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) + return + } + + methodNotAllowed(w, "PUT", "DELETE") + return +} + type jwkset = struct { Keys []map[string]any `json:"keys"` } diff --git a/webserver/api_test.go b/webserver/api_test.go index dad34b7..db2f99d 100644 --- a/webserver/api_test.go +++ b/webserver/api_test.go @@ -168,6 +168,13 @@ func TestApi(t *testing.T) { t.Errorf("Get groups: %v %#v", err, s) } + resp, err = do("PUT", "/galene-api/0/.groups/test/.fallback-users", + "application/json", "", "", + `[{"password": "topsecret"}]`) + if err != nil || resp.StatusCode != http.StatusNoContent { + t.Errorf("Set fallback users: %v %v", err, resp.StatusCode) + } + resp, err = do("PUT", "/galene-api/0/.groups/test/.keys", "application/jwk-set+json", "", "", `{"keys": [{ @@ -261,10 +268,20 @@ func TestApi(t *testing.T) { t.Errorf("Users (after delete): %#v", desc.Users) } + if len(desc.FallbackUsers) != 1 { + t.Errorf("Keys: %v", len(desc.AuthKeys)) + } + if len(desc.AuthKeys) != 1 { t.Errorf("Keys: %v", len(desc.AuthKeys)) } + resp, err = do("DELETE", "/galene-api/0/.groups/test/.fallback-users", + "", "", "", "") + if err != nil || resp.StatusCode != http.StatusNoContent { + t.Errorf("Delete fallback users: %v %v", err, resp.StatusCode) + } + resp, err = do("DELETE", "/galene-api/0/.groups/test/.keys", "", "", "", "") if err != nil || resp.StatusCode != http.StatusNoContent {