diff --git a/README b/README index f34f226..8d99f48 100644 --- a/README +++ b/README @@ -126,27 +126,32 @@ with the permission `present`: { "users":{ - "jch": {"password":"1234", "permissions": "op"} + "jch": {"password": "1234", "permissions": "op"} "john": {"password": "secret", "permissions": "present"} } } If the group is to be publicly accessible, you may allow logins with any -username using the `fallback-users` entry:: +username using the `wildcard-user` entry:: { "users":{ "jch": {"password":"1234", "permissions": "op"} }, - "fallback-users": [ - {"password": {"type": "wildcard"}, "permissions": "present"} - ], + "wildcard-user": {"password": "1234", "permissions": "present"}, "public": true } -The password `{"type": "wildcard"}` indicates that any password will be -accepted. +If you want to allow users to use any password, use a wildcard password: + { + "users":{ + "jch": {"password":"1234", "permissions": "op"} + }, + "wildcard-user": + {"password": {"type": "wildcard"}, "permissions": "present"}, + "public": true + } ## Reference @@ -158,9 +163,9 @@ nobody will be able to join the group. The following fields are allowed: - `users`: is a dictionary that maps user names to dictionaries with entries `password` and `permissions`; `permissions` should be one of `op`, `present` or `passive`; - - `fallback-users` is an array of dictionaries with entries `password` - and `permissions` that will be used for usernames with no matching - entry in the `users` dictionary; + - `wildcard-user` is a dictionaries with entries `password` and `permissions` + that will be used for usernames with no matching entry in the `users` + dictionary; - `authKeys`, `authServer` and `authPortal`: see *Authorisation* below; - `public`: if true, then the group is listed on the landing page; - `displayName`: a human-friendly version of the group name; diff --git a/README.API b/README.API index 9c7db8e..b5e9982 100644 --- a/README.API +++ b/README.API @@ -55,14 +55,6 @@ 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/v0/.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/v0/.groups/groupname/.keys @@ -97,6 +89,21 @@ will be hashed on the server. Allowed methods are PUT, POST and DELETE. Accepted content-types are `application/json` for PUT and `text/plain` for POST. +### Wildcard user + + /galene-api/v0/.groups/groupname/.wildcard-user + +Contains a dictionary defining the wildcard user, in the same format as +the dictionary defining an ordinary user. Allowed methods are HEAD, GET, +PUT and DELETE. + +### Wildcard user password + + /galene-api/v0/.groups/groupname/.wildcard-user/.password + +This is analogous to the password of an ordinary user. Allowed methods +are PUT, POST and DELETE. + ### List of stateful tokens /galene-api/v0/.groups/groupname/.users/username/.tokens/ diff --git a/group/description.go b/group/description.go index ad50ef4..8bb2ac3 100644 --- a/group/description.go +++ b/group/description.go @@ -192,8 +192,8 @@ type Description struct { // Users allowed to login Users map[string]UserDescription `json:"users,omitempty"` - // Credentials for users with arbitrary username - FallbackUsers []UserDescription `json:"fallback-users,omitempty"` + // Credentials for user with arbitrary username + WildcardUser *UserDescription `json:"wildcard-user,omitempty"` // The (public) keys used for token authentication. AuthKeys []map[string]interface{} `json:"authKeys,omitempty"` @@ -298,7 +298,7 @@ func GetSanitisedDescription(name string) (*Description, string, error) { desc := *d desc.Users = nil - desc.FallbackUsers = nil + desc.WildcardUser = nil desc.AuthKeys = nil return &desc, makeETag(desc.fileSize, desc.modTime), nil } @@ -335,7 +335,7 @@ func DeleteDescription(name, etag string) error { // UpdateDescription overwrites a description if it matches a given ETag. // In order to create a new group, pass an empty ETag. func UpdateDescription(name, etag string, desc *Description) error { - if desc.Users != nil || desc.FallbackUsers != nil || desc.AuthKeys != nil { + if desc.Users != nil || desc.WildcardUser != nil || desc.AuthKeys != nil { return errors.New("description is not sanitised") } @@ -364,7 +364,7 @@ func UpdateDescription(name, etag string, desc *Description) error { newdesc := *desc if old != nil { newdesc.Users = old.Users - newdesc.FallbackUsers = old.FallbackUsers + newdesc.WildcardUser = old.WildcardUser newdesc.AuthKeys = old.AuthKeys } @@ -500,8 +500,13 @@ func upgradeDescription(desc *Description) error { } for _, u := range ps { if u.Username == "" { - desc.FallbackUsers = append(desc.FallbackUsers, - upgradeUser(u, p)) + if desc.WildcardUser != nil { + log.Printf("%v: duplicate wildcard user", + desc.FileName) + continue + } + u := upgradeUser(u, p) + desc.WildcardUser = &u continue } _, found := desc.Users[u.Username] @@ -559,7 +564,7 @@ func GetDescriptionNames() ([]string, error) { return names, err } -func SetFallbackUsers(group string, users []UserDescription) error { +func SetWildcardUser(group string, user *UserDescription) error { groups.mu.Lock() defer groups.mu.Unlock() @@ -567,7 +572,7 @@ func SetFallbackUsers(group string, users []UserDescription) error { if err != nil { return err } - desc.FallbackUsers = users + desc.WildcardUser = user return rewriteDescriptionFile(desc.FileName, desc) } diff --git a/group/description_test.go b/group/description_test.go index 96be786..06fbb5c 100644 --- a/group/description_test.go +++ b/group/description_test.go @@ -66,9 +66,8 @@ var descJSON = ` "james": {"password": "secret2", "permissions": "observe"}, "peter": {"password": "secret4"} }, - "fallback-users": [ + "wildcard-user": {"permissions": "observe", "password": {"type":"wildcard"}} - ] }` func TestDescriptionJSON(t *testing.T) { @@ -150,19 +149,15 @@ func TestUpgradeDescription(t *testing.T) { } } - if len(d1.FallbackUsers) != len(d2.FallbackUsers) { - t.Errorf("length not equal: %v != %v", - len(d1.FallbackUsers), len(d2.FallbackUsers)) - } - - for k, v1 := range d1.FallbackUsers { - v2 := d2.FallbackUsers[k] - if !reflect.DeepEqual(v1.Password, v2.Password) || - !permissionsEqual( - v1.Permissions.Permissions(&d1), - v2.Permissions.Permissions(&d2), - ) { - t.Errorf("%v not equal: %v != %v", k, v1, v2) + if d1.WildcardUser != nil || d2.WildcardUser != nil { + if !reflect.DeepEqual( + d1.WildcardUser.Password, d2.WildcardUser.Password, + ) || !permissionsEqual( + d1.WildcardUser.Permissions.Permissions(&d1), + d2.WildcardUser.Permissions.Permissions(&d2), + ) { + t.Errorf("WildcardUser not equal: %v != %v", + d1.WildcardUser, d2.WildcardUser) } } } diff --git a/group/group.go b/group/group.go index 2e899f9..191706a 100644 --- a/group/group.go +++ b/group/group.go @@ -942,26 +942,12 @@ func (g *Group) getPasswordPermission(creds ClientCredentials) (Permissions, err } } - for _, c := range desc.FallbackUsers { - if c.Password.Type == "wildcard" { - continue - } - ok, _ := c.Password.Match(creds.Password) + if desc.WildcardUser != nil { + ok, _ := desc.WildcardUser.Password.Match(creds.Password) if ok { - return c.Permissions, nil + return desc.WildcardUser.Permissions, nil } } - - for _, c := range desc.FallbackUsers { - if c.Password.Type != "wildcard" { - continue - } - ok, _ := c.Password.Match(creds.Password) - if ok { - return c.Permissions, nil - } - } - return Permissions{}, &NotAuthorisedError{} } diff --git a/webserver/api.go b/webserver/api.go index 7f908a6..8f8bae0 100644 --- a/webserver/api.go +++ b/webserver/api.go @@ -161,9 +161,6 @@ func apiGroupHandler(w http.ResponseWriter, r *http.Request, pth string) { if kind == ".users" { usersHandler(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 @@ -425,38 +422,6 @@ 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" { - var users []group.UserDescription - done := getJSON(w, r, &users) - if done { - 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 1e5418b..9c1a661 100644 --- a/webserver/api_test.go +++ b/webserver/api_test.go @@ -159,13 +159,6 @@ func TestApi(t *testing.T) { t.Errorf("Get groups: %v %v", err, groups) } - resp, err = do("PUT", "/galene-api/v0/.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/v0/.groups/test/.keys", "application/jwk-set+json", "", "", `{"keys": [{ @@ -260,10 +253,6 @@ 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)) } @@ -344,12 +333,6 @@ func TestApi(t *testing.T) { t.Errorf("Token list: %v %v", tokens, err) } - resp, err = do("DELETE", "/galene-api/v0/.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/v0/.groups/test/.keys", "", "", "", "") if err != nil || resp.StatusCode != http.StatusNoContent {