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

23
README
View File

@ -132,21 +132,26 @@ with the permission `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;

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
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/

View File

@ -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)
}

View File

@ -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),
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("%v not equal: %v != %v", k, v1, v2)
t.Errorf("WildcardUser not equal: %v != %v",
d1.WildcardUser, d2.WildcardUser)
}
}
}

View File

@ -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{}
}

View File

@ -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"`
}

View File

@ -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 {