Commit 9eb03640 authored by Juliusz Chroboczek's avatar Juliusz Chroboczek

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.
parent f5279022
...@@ -126,27 +126,32 @@ with the permission `present`: ...@@ -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: ...@@ -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;
......
...@@ -55,14 +55,6 @@ on-disk format but without any user definitions or cryptographic keys. ...@@ -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. ...@@ -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/
......
...@@ -192,8 +192,8 @@ type Description struct { ...@@ -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) { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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) { ...@@ -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 { ...@@ -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)
} }
......
...@@ -66,9 +66,8 @@ var descJSON = ` ...@@ -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) { ...@@ -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)
} }
} }
} }
......
...@@ -942,26 +942,12 @@ func (g *Group) getPasswordPermission(creds ClientCredentials) (Permissions, err ...@@ -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{}
} }
......
...@@ -161,9 +161,6 @@ func apiGroupHandler(w http.ResponseWriter, r *http.Request, pth string) { ...@@ -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) { ...@@ -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"`
} }
......
...@@ -159,13 +159,6 @@ func TestApi(t *testing.T) { ...@@ -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) { ...@@ -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) { ...@@ -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 {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment