mirror of
https://github.com/jech/galene.git
synced 2024-12-22 15:25:48 +01:00
Implement accessors for description files.
Allow reading and modifying description files, in a manner that aligns with the needs of the API.
This commit is contained in:
parent
e14eec86d3
commit
fc6387bb38
4 changed files with 411 additions and 4 deletions
2
README
2
README
|
@ -75,6 +75,8 @@ The fields are as follows:
|
|||
- `users` defines the users allowed to administer the server, and has the
|
||||
same syntax as user definitions in groups (see below), except that the
|
||||
only meaningful permission is `"admin"`;
|
||||
- `writableGroups`: if true, then the API can modify group description
|
||||
files; by default, group files are treated as read-only;
|
||||
- `publicServer`: if true, then cross-origin access to the server is
|
||||
allowed. This is safe if the server is on the public Internet, but not
|
||||
necessarily so if it is on a private network.
|
||||
|
|
|
@ -3,6 +3,8 @@ package group
|
|||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -11,6 +13,9 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
var ErrTagMismatch = errors.New("tag mismatch")
|
||||
var ErrDescriptionsNotWritable = &NotAuthorisedError{}
|
||||
|
||||
type Permissions struct {
|
||||
// non-empty for a named permissions set
|
||||
name string
|
||||
|
@ -123,6 +128,9 @@ type Description struct {
|
|||
modTime time.Time `json:"-"`
|
||||
fileSize int64 `json:"-"`
|
||||
|
||||
// Whether this is an automatically generated subgroup
|
||||
isSubgroup bool `json:"-"`
|
||||
|
||||
// The user-friendly group name
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
|
||||
|
@ -265,6 +273,133 @@ func GetDescription(name string) (*Description, error) {
|
|||
return readDescription(name, true)
|
||||
}
|
||||
|
||||
// GetSanitisedDescription returns the subset of the description that is
|
||||
// published on the web interface together with a suitable ETag.
|
||||
func GetSanitisedDescription(name string) (*Description, string, error) {
|
||||
d, err := GetDescription(name)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if d.isSubgroup {
|
||||
return nil, "", os.ErrNotExist
|
||||
}
|
||||
|
||||
desc := *d
|
||||
desc.Users = nil
|
||||
desc.FallbackUsers = nil
|
||||
desc.AuthKeys = nil
|
||||
return &desc, makeETag(desc.fileSize, desc.modTime), nil
|
||||
}
|
||||
|
||||
// GetDescriptionTag returns an ETag for a description.
|
||||
func GetDescriptionTag(name string) (string, error) {
|
||||
fi, _, _, err := getDescriptionFile(name, false, os.Stat)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return makeETag(fi.Size(), fi.ModTime()), nil
|
||||
}
|
||||
|
||||
func makeETag(fileSize int64, modTime time.Time) string {
|
||||
return fmt.Sprintf("\"%v-%v\"", fileSize, modTime.UnixNano())
|
||||
}
|
||||
|
||||
// DeleteDescription deletes a description (and therefore persistently
|
||||
// deletes a group) but only if it matches a given ETag.
|
||||
func DeleteDescription(name, etag string) error {
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
fi, fileName, _, err := getDescriptionFile(name, false, os.Stat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if etag != makeETag(fi.Size(), fi.ModTime()) {
|
||||
return ErrTagMismatch
|
||||
}
|
||||
return os.Remove(fileName)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return errors.New("description is not sanitised")
|
||||
}
|
||||
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
oldetag := ""
|
||||
var filename string
|
||||
old, err := readDescription(name, false)
|
||||
if err == nil {
|
||||
oldetag = makeETag(old.fileSize, old.modTime)
|
||||
filename = old.FileName
|
||||
} else if os.IsNotExist(err) {
|
||||
old = nil
|
||||
filename = filepath.Join(
|
||||
Directory, path.Clean("/"+name)+".json",
|
||||
)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldetag != etag {
|
||||
return ErrTagMismatch
|
||||
}
|
||||
|
||||
newdesc := *desc
|
||||
if old != nil {
|
||||
newdesc.Users = old.Users
|
||||
newdesc.FallbackUsers = old.FallbackUsers
|
||||
newdesc.AuthKeys = old.AuthKeys
|
||||
}
|
||||
|
||||
return rewriteDescriptionFile(filename, &newdesc)
|
||||
}
|
||||
|
||||
func rewriteDescriptionFile(filename string, desc *Description) error {
|
||||
conf, err := GetConfiguration()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !conf.WritableGroups {
|
||||
return ErrDescriptionsNotWritable
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp(path.Dir(filename), "*.temp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temp := f.Name()
|
||||
|
||||
encoder := json.NewEncoder(f)
|
||||
err = encoder.Encode(desc)
|
||||
if err == nil {
|
||||
err = f.Sync()
|
||||
}
|
||||
if err != nil {
|
||||
f.Close()
|
||||
os.Remove(temp)
|
||||
return err
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
os.Remove(temp)
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Rename(temp, filename)
|
||||
if err != nil {
|
||||
os.Remove(temp)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// readDescription reads a group's description from disk
|
||||
func readDescription(name string, allowSubgroups bool) (*Description, error) {
|
||||
r, fileName, isSubgroup, err :=
|
||||
|
@ -291,6 +426,7 @@ func readDescription(name string, allowSubgroups bool) (*Description, error) {
|
|||
if !desc.AutoSubgroups {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
desc.isSubgroup = true
|
||||
desc.Public = false
|
||||
desc.Description = ""
|
||||
}
|
||||
|
@ -374,3 +510,150 @@ func upgradeDescription(desc *Description) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDescriptionNames() ([]string, error) {
|
||||
var names []string
|
||||
err := filepath.WalkDir(
|
||||
Directory,
|
||||
func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
base := filepath.Base(path)
|
||||
if d.IsDir() {
|
||||
if base[0] == '.' {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if base[0] == '.' {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(base, ".json") {
|
||||
names = append(names, strings.TrimSuffix(
|
||||
base, ".json",
|
||||
))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
return names, err
|
||||
}
|
||||
|
||||
func GetUsers(group string) ([]string, string, error) {
|
||||
desc, err := GetDescription(group)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
users := make([]string, 0, len(desc.Users))
|
||||
for u := range desc.Users {
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
return users, makeETag(desc.fileSize, desc.modTime), nil
|
||||
}
|
||||
|
||||
func GetSanitisedUser(group, username string) (UserDescription, string, error) {
|
||||
desc, err := GetDescription(group)
|
||||
if err != nil {
|
||||
return UserDescription{}, "", err
|
||||
}
|
||||
|
||||
if desc.Users == nil {
|
||||
return UserDescription{}, "", os.ErrNotExist
|
||||
}
|
||||
|
||||
u, ok := desc.Users[username]
|
||||
if !ok {
|
||||
return UserDescription{}, "", os.ErrNotExist
|
||||
}
|
||||
|
||||
u.Password = Password{}
|
||||
return u, makeETag(desc.fileSize, desc.modTime), nil
|
||||
}
|
||||
|
||||
func GetUserTag(group, username string) (string, error) {
|
||||
_, etag, err := GetSanitisedUser(group, username)
|
||||
return etag, err
|
||||
}
|
||||
|
||||
func DeleteUser(group, username, etag string) error {
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
desc, err := readDescription(group, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if desc.Users == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
_, ok := desc.Users[username]
|
||||
if !ok {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
oldetag := makeETag(desc.fileSize, desc.modTime)
|
||||
if oldetag != etag {
|
||||
return ErrTagMismatch
|
||||
}
|
||||
|
||||
delete(desc.Users, username)
|
||||
return rewriteDescriptionFile(desc.FileName, desc)
|
||||
}
|
||||
|
||||
func UpdateUser(group, username, etag string, user *UserDescription) error {
|
||||
if user.Password.Type != "" || user.Password.Key != "" {
|
||||
return errors.New("user description is not sanitised")
|
||||
}
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
desc, err := readDescription(group, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if desc.Users == nil {
|
||||
desc.Users = make(map[string]UserDescription)
|
||||
}
|
||||
|
||||
old, ok := desc.Users[username]
|
||||
var oldetag string
|
||||
if ok {
|
||||
oldetag = makeETag(desc.fileSize, desc.modTime)
|
||||
} else {
|
||||
oldetag = ""
|
||||
}
|
||||
|
||||
if oldetag != etag {
|
||||
return ErrTagMismatch
|
||||
}
|
||||
|
||||
user.Password = old.Password
|
||||
desc.Users[username] = *user
|
||||
return rewriteDescriptionFile(desc.FileName, desc)
|
||||
}
|
||||
|
||||
func SetUserPassword(group, username string, pw Password) error {
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
desc, err := readDescription(group, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if desc.Users == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
user, ok := desc.Users[username]
|
||||
if !ok {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
user.Password = pw
|
||||
desc.Users[username] = user
|
||||
return rewriteDescriptionFile(desc.FileName, desc)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package group
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
@ -95,3 +97,121 @@ func TestUpgradeDescription(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonWritableGroups(t *testing.T) {
|
||||
Directory = t.TempDir()
|
||||
configuration.configuration = &Configuration{}
|
||||
|
||||
_, err := GetDescription("test")
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("GetDescription: got %#v, expected ErrNotExist", err)
|
||||
}
|
||||
|
||||
err = UpdateDescription("test", "", &Description{})
|
||||
if !errors.Is(err, ErrDescriptionsNotWritable) {
|
||||
t.Errorf("UpdateDescription: got %#v, " +
|
||||
"expected ErrDescriptionsNotWritable", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritableGroups(t *testing.T) {
|
||||
Directory = t.TempDir()
|
||||
configuration.configuration = &Configuration{
|
||||
WritableGroups: true,
|
||||
}
|
||||
|
||||
_, err := GetDescription("test")
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("GetDescription: got %v, expected ErrNotExist", err)
|
||||
}
|
||||
|
||||
err = UpdateDescription("test", "\"etag\"", &Description{})
|
||||
if !errors.Is(err, ErrTagMismatch) {
|
||||
t.Errorf("UpdateDescription: got %v, expected ErrTagMismatch",
|
||||
err)
|
||||
}
|
||||
|
||||
err = UpdateDescription("test", "", &Description{})
|
||||
if err != nil {
|
||||
t.Errorf("UpdateDescription: got %v", err)
|
||||
}
|
||||
|
||||
_, err = GetDescription("test")
|
||||
if err != nil {
|
||||
t.Errorf("GetDescription: got %v", err)
|
||||
}
|
||||
|
||||
desc, token, err := GetSanitisedDescription("test")
|
||||
if err != nil || token == "" {
|
||||
t.Errorf("GetSanitisedDescription: got %v", err)
|
||||
}
|
||||
|
||||
desc.DisplayName = "Test"
|
||||
|
||||
err = UpdateDescription("test", "\"badetag\"", desc)
|
||||
if !errors.Is(err, ErrTagMismatch) {
|
||||
t.Errorf("UpdateDescription: got %v, expected ErrTagMismatch",
|
||||
err)
|
||||
}
|
||||
|
||||
err = UpdateDescription("test", token, desc)
|
||||
if err != nil {
|
||||
t.Errorf("UpdateDescription: got %v", err)
|
||||
}
|
||||
|
||||
desc, err = GetDescription("test")
|
||||
if err != nil || desc.DisplayName != "Test" {
|
||||
t.Errorf("GetDescription: expected %v %v, got %v %v",
|
||||
nil, "Test", err, desc.AllowAnonymous,
|
||||
)
|
||||
}
|
||||
|
||||
_, _, err = GetSanitisedUser("test", "jch")
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("GetSanitisedUser: got %v, expected ErrNotExist", err)
|
||||
}
|
||||
|
||||
err = UpdateUser("test", "jch", "", &UserDescription{
|
||||
Permissions: Permissions{name: "observe"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("UpdateUser: got %v", err)
|
||||
}
|
||||
|
||||
user, token, err := GetSanitisedUser("test", "jch")
|
||||
if err != nil || token == "" || user.Permissions.name != "observe" {
|
||||
t.Errorf("GetDescription: got %v %v, expected %v %v",
|
||||
err, user.Permissions.name, nil, "observe",
|
||||
)
|
||||
}
|
||||
|
||||
err = UpdateUser("test", "jch", "", &UserDescription{
|
||||
Permissions: Permissions{name: "present"},
|
||||
})
|
||||
if !errors.Is(err, ErrTagMismatch) {
|
||||
t.Errorf("UpdateDescription: got %v, expected ErrTagMismatch",
|
||||
err)
|
||||
}
|
||||
|
||||
err = UpdateUser("test", "jch", token, &UserDescription{
|
||||
Permissions: Permissions{name: "present"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("UpdateUser: got %v", err)
|
||||
}
|
||||
|
||||
err = SetUserPassword("test", "jch", Password{
|
||||
Type: "",
|
||||
Key: "pw",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("SetUserPassword: got %v", err)
|
||||
}
|
||||
|
||||
desc, err = GetDescription("test")
|
||||
if err != nil || desc.Users["jch"].Password.Key != "pw" {
|
||||
t.Errorf("GetDescription: got %v %v, expected %v %v",
|
||||
err, desc.Users["jch"].Password.Key, nil, "pw",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ var UDPMin, UDPMax uint16
|
|||
type NotAuthorisedError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (err *NotAuthorisedError) Error() string {
|
||||
if err.err != nil {
|
||||
return "not authorised: " + err.err.Error()
|
||||
|
@ -854,10 +855,11 @@ type Configuration struct {
|
|||
modTime time.Time `json:"-"`
|
||||
fileSize int64 `json:"-"`
|
||||
|
||||
PublicServer bool `json:"publicServer"`
|
||||
CanonicalHost string `json:"canonicalHost"`
|
||||
ProxyURL string `json:"proxyURL"`
|
||||
Users map[string]UserDescription
|
||||
PublicServer bool `json:"publicServer"`
|
||||
CanonicalHost string `json:"canonicalHost"`
|
||||
ProxyURL string `json:"proxyURL"`
|
||||
WritableGroups bool `json:"writableGroups"`
|
||||
Users map[string]UserDescription
|
||||
|
||||
// obsolete fields
|
||||
Admin []ClientPattern `json:"admin"`
|
||||
|
|
Loading…
Reference in a new issue