mirror of
https://github.com/jech/galene.git
synced 2024-11-09 18:25:58 +01:00
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:
parent
f5279022ce
commit
9eb0364016
7 changed files with 57 additions and 111 deletions
25
README
25
README
|
@ -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;
|
||||||
|
|
23
README.API
23
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
|
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/
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue