1
Fork 0

Allow a single wildcard user.

Rename the fallback-users entry to wildcard-user, and only
allow a single fallback user.  This is missing the HTTP API.
This commit is contained in:
Juliusz Chroboczek 2024-05-03 19:12:12 +02:00
parent f5279022ce
commit 9eb0364016
7 changed files with 57 additions and 111 deletions

25
README
View File

@ -126,27 +126,32 @@ with the permission `present`:
{ {
"users":{ "users":{
"jch": {"password":"1234", "permissions": "op"} "jch": {"password": "1234", "permissions": "op"}
"john": {"password": "secret", "permissions": "present"} "john": {"password": "secret", "permissions": "present"}
} }
} }
If the group is to be publicly accessible, you may allow logins with any 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":{ "users":{
"jch": {"password":"1234", "permissions": "op"} "jch": {"password":"1234", "permissions": "op"}
}, },
"fallback-users": [ "wildcard-user": {"password": "1234", "permissions": "present"},
{"password": {"type": "wildcard"}, "permissions": "present"}
],
"public": true "public": true
} }
The password `{"type": "wildcard"}` indicates that any password will be If you want to allow users to use any password, use a wildcard password:
accepted.
{
"users":{
"jch": {"password":"1234", "permissions": "op"}
},
"wildcard-user":
{"password": {"type": "wildcard"}, "permissions": "present"},
"public": true
}
## Reference ## 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 - `users`: is a dictionary that maps user names to dictionaries with
entries `password` and `permissions`; `permissions` should be one of entries `password` and `permissions`; `permissions` should be one of
`op`, `present` or `passive`; `op`, `present` or `passive`;
- `fallback-users` is an array of dictionaries with entries `password` - `wildcard-user` is a dictionaries with entries `password` and `permissions`
and `permissions` that will be used for usernames with no matching that will be used for usernames with no matching entry in the `users`
entry in the `users` dictionary; dictionary;
- `authKeys`, `authServer` and `authPortal`: see *Authorisation* below; - `authKeys`, `authServer` and `authPortal`: see *Authorisation* below;
- `public`: if true, then the group is listed on the landing page; - `public`: if true, then the group is listed on the landing page;
- `displayName`: a human-friendly version of the group name; - `displayName`: a human-friendly version of the group name;

View File

@ -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 Allowed methods are HEAD, GET, PUT and DELETE. The only accepted
content-type is `application/json`. 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 ### Authentication keys
/galene-api/v0/.groups/groupname/.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 Accepted content-types are `application/json` for PUT and `text/plain` for
POST. 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 ### List of stateful tokens
/galene-api/v0/.groups/groupname/.users/username/.tokens/ /galene-api/v0/.groups/groupname/.users/username/.tokens/

View File

@ -192,8 +192,8 @@ type Description struct {
// Users allowed to login // Users allowed to login
Users map[string]UserDescription `json:"users,omitempty"` Users map[string]UserDescription `json:"users,omitempty"`
// Credentials for users with arbitrary username // Credentials for user with arbitrary username
FallbackUsers []UserDescription `json:"fallback-users,omitempty"` WildcardUser *UserDescription `json:"wildcard-user,omitempty"`
// The (public) keys used for token authentication. // The (public) keys used for token authentication.
AuthKeys []map[string]interface{} `json:"authKeys,omitempty"` AuthKeys []map[string]interface{} `json:"authKeys,omitempty"`
@ -298,7 +298,7 @@ func GetSanitisedDescription(name string) (*Description, string, error) {
desc := *d desc := *d
desc.Users = nil desc.Users = nil
desc.FallbackUsers = nil desc.WildcardUser = nil
desc.AuthKeys = nil desc.AuthKeys = nil
return &desc, makeETag(desc.fileSize, desc.modTime), 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. // UpdateDescription overwrites a description if it matches a given ETag.
// In order to create a new group, pass an empty ETag. // In order to create a new group, pass an empty ETag.
func UpdateDescription(name, etag string, desc *Description) error { 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") return errors.New("description is not sanitised")
} }
@ -364,7 +364,7 @@ func UpdateDescription(name, etag string, desc *Description) error {
newdesc := *desc newdesc := *desc
if old != nil { if old != nil {
newdesc.Users = old.Users newdesc.Users = old.Users
newdesc.FallbackUsers = old.FallbackUsers newdesc.WildcardUser = old.WildcardUser
newdesc.AuthKeys = old.AuthKeys newdesc.AuthKeys = old.AuthKeys
} }
@ -500,8 +500,13 @@ func upgradeDescription(desc *Description) error {
} }
for _, u := range ps { for _, u := range ps {
if u.Username == "" { if u.Username == "" {
desc.FallbackUsers = append(desc.FallbackUsers, if desc.WildcardUser != nil {
upgradeUser(u, p)) log.Printf("%v: duplicate wildcard user",
desc.FileName)
continue
}
u := upgradeUser(u, p)
desc.WildcardUser = &u
continue continue
} }
_, found := desc.Users[u.Username] _, found := desc.Users[u.Username]
@ -559,7 +564,7 @@ func GetDescriptionNames() ([]string, error) {
return names, err return names, err
} }
func SetFallbackUsers(group string, users []UserDescription) error { func SetWildcardUser(group string, user *UserDescription) error {
groups.mu.Lock() groups.mu.Lock()
defer groups.mu.Unlock() defer groups.mu.Unlock()
@ -567,7 +572,7 @@ func SetFallbackUsers(group string, users []UserDescription) error {
if err != nil { if err != nil {
return err return err
} }
desc.FallbackUsers = users desc.WildcardUser = user
return rewriteDescriptionFile(desc.FileName, desc) return rewriteDescriptionFile(desc.FileName, desc)
} }

View File

@ -66,9 +66,8 @@ var descJSON = `
"james": {"password": "secret2", "permissions": "observe"}, "james": {"password": "secret2", "permissions": "observe"},
"peter": {"password": "secret4"} "peter": {"password": "secret4"}
}, },
"fallback-users": [ "wildcard-user":
{"permissions": "observe", "password": {"type":"wildcard"}} {"permissions": "observe", "password": {"type":"wildcard"}}
]
}` }`
func TestDescriptionJSON(t *testing.T) { func TestDescriptionJSON(t *testing.T) {
@ -150,19 +149,15 @@ func TestUpgradeDescription(t *testing.T) {
} }
} }
if len(d1.FallbackUsers) != len(d2.FallbackUsers) { if d1.WildcardUser != nil || d2.WildcardUser != nil {
t.Errorf("length not equal: %v != %v", if !reflect.DeepEqual(
len(d1.FallbackUsers), len(d2.FallbackUsers)) d1.WildcardUser.Password, d2.WildcardUser.Password,
} ) || !permissionsEqual(
d1.WildcardUser.Permissions.Permissions(&d1),
for k, v1 := range d1.FallbackUsers { d2.WildcardUser.Permissions.Permissions(&d2),
v2 := d2.FallbackUsers[k] ) {
if !reflect.DeepEqual(v1.Password, v2.Password) || t.Errorf("WildcardUser not equal: %v != %v",
!permissionsEqual( d1.WildcardUser, d2.WildcardUser)
v1.Permissions.Permissions(&d1),
v2.Permissions.Permissions(&d2),
) {
t.Errorf("%v not equal: %v != %v", k, v1, v2)
} }
} }
} }

View File

@ -942,26 +942,12 @@ func (g *Group) getPasswordPermission(creds ClientCredentials) (Permissions, err
} }
} }
for _, c := range desc.FallbackUsers { if desc.WildcardUser != nil {
if c.Password.Type == "wildcard" { ok, _ := desc.WildcardUser.Password.Match(creds.Password)
continue
}
ok, _ := c.Password.Match(creds.Password)
if ok { 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{} return Permissions{}, &NotAuthorisedError{}
} }

View File

@ -161,9 +161,6 @@ func apiGroupHandler(w http.ResponseWriter, r *http.Request, pth string) {
if kind == ".users" { if kind == ".users" {
usersHandler(w, r, g, rest) usersHandler(w, r, g, rest)
return return
} else if kind == ".fallback-users" && rest == "" {
fallbackUsersHandler(w, r, g)
return
} else if kind == ".keys" && rest == "" { } else if kind == ".keys" && rest == "" {
keysHandler(w, r, g) keysHandler(w, r, g)
return return
@ -425,38 +422,6 @@ func passwordHandler(w http.ResponseWriter, r *http.Request, g, user string) {
return 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 { type jwkset = struct {
Keys []map[string]any `json:"keys"` Keys []map[string]any `json:"keys"`
} }

View File

@ -159,13 +159,6 @@ func TestApi(t *testing.T) {
t.Errorf("Get groups: %v %v", err, groups) 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", resp, err = do("PUT", "/galene-api/v0/.groups/test/.keys",
"application/jwk-set+json", "", "", "application/jwk-set+json", "", "",
`{"keys": [{ `{"keys": [{
@ -260,10 +253,6 @@ func TestApi(t *testing.T) {
t.Errorf("Users (after delete): %#v", desc.Users) 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 { if len(desc.AuthKeys) != 1 {
t.Errorf("Keys: %v", len(desc.AuthKeys)) t.Errorf("Keys: %v", len(desc.AuthKeys))
} }
@ -344,12 +333,6 @@ func TestApi(t *testing.T) {
t.Errorf("Token list: %v %v", tokens, err) 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", resp, err = do("DELETE", "/galene-api/v0/.groups/test/.keys",
"", "", "", "") "", "", "", "")
if err != nil || resp.StatusCode != http.StatusNoContent { if err != nil || resp.StatusCode != http.StatusNoContent {