1
Fork 0

Rework configuration file format.

The "users" entry is now a dictionary mapping user names to
passwords and permissions.  In order to allow for wildcards,
there is a new type of password, the wildcard password, and
an extra array called "fallback-users".

The field "allow-anonymous" no longer exists, this is now
the default behaviour.  The field "allow-subgroups" has been
renamed to "auto-subgroups".

We provide backwards compatibility for group definition files,
but not for the config.json file, where the old "admin" array
is simply ignored.
This commit is contained in:
Juliusz Chroboczek 2024-01-02 18:36:09 +01:00
parent eb54f2a9bb
commit d887a216f0
10 changed files with 571 additions and 204 deletions

106
README
View File

@ -66,14 +66,15 @@ The server may be configured in the JSON file `data/config.json`. This
file may look as follows:
{
"admin":[{"username":"root","password":"secret"}],
"users":{"root": {"password":"secret", "permissions": "admin"}},
"canonicalHost": "galene.example.org"
}
The fields are as follows:
- `admin` defines the users allowed to look at the `/stats.html` file; it
has the same syntax as user definitions in groups (see below).
- `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"`;
- `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.
@ -100,38 +101,45 @@ a file `groups/teaching/networking.json` defines a group called
A typical group definition file looks like this:
{
"op":[{"username":"jch","password":"1234"}],
"users":{
"jch": {"password":"1234", "permissions": "op"}
},
"allow-recording": true,
"allow-subgroups": true
"auto-subgroups": true
}
This defines a group with the operator (administrator) username *jch* and
password *1234*. The `allow-recording` entry says that the operator is
allowed to record videos to disk, and the `allow-subgroups` entry says
allowed to record videos to disk, and the `auto-subgroups` entry says
that subgroups will be created automatically. This particular group does
not allow password login for ordinary users, and is suitable if you use
invitations (see *Stateful Tokens* below) for ordinary users.
In order to allow password login for ordinary users, add a list of users
as the entry `presenter`:
In order to allow password login for ordinary users, add password entries
with the permission `present`:
{
"op": [{"username":"jch","password":"1234"}],
"presenter": [{"username":"john", "password": "secret"}]
"users":{
"jch": {"password":"1234", "permissions": "op"}
"john": {"password": "secret", "permissions": "present"}
}
}
If the group is to be publicly accessible, you may allow logins with any
username and an empty password:
username using the `fallback-users` entry::
{
"op": [{"username":"jch","password":"1234"}],
"presenter": [{}],
"users":{
"jch": {"password":"1234", "permissions": "op"}
},
"fallback-users": [
{"password": {"type": "wildcard"}, "permissions": "present"}
],
"public": true
}
The empty dictionary `{}` is a wildcard entry: it matches any username and
any password. Setting `public` causes the group to be displayed in the
list of public groups on the landing page
The password `{"type": "wildcard"}` indicates that any password will be
accepted.
## Reference
@ -141,10 +149,12 @@ entries between `{' and `}'). All fields are optional, but unless you
specify at least one user definition (`op`, `presenter`, or `other`),
nobody will be able to join the group. The following fields are allowed:
- `op`, `presenter`, `other`: each of these is an array of user
definitions (see *Authorisation* below) and specifies the users allowed
to connect respectively with operator privileges, with presenter
privileges, and as passive listeners;
- `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;
- `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;
@ -163,7 +173,7 @@ nobody will be able to join the group. The following fields are allowed:
- `unrestricted-tokens`: if true, then ordinary users (without the "op"
privilege) are allowed to create tokens;
- `allow-anonymous`: if true, then users may connect with an empty username;
- `allow-subgroups`: if true, then subgroups of the form `group/subgroup`
- `auto-subgroups`: if true, then subgroups of the form `group/subgroup`
are automatically created when first accessed;
- `autolock`: if true, the group will start locked and become locked
whenever there are no clients with operator privileges;
@ -202,33 +212,34 @@ even Unix passwords).
### Password authorisation
When password authorisation is used, authorised usernames and password are
defined directly in the group configuration file, in the `op`, `presenter`
and `other` arrays. Each member of the array is a dictionary, that may
contain the fields `username` and `password`:
defined directly in the group configuration file, in the `users` and
`fallback-users` entries. The `users` entry is a dictionary that maps
user names to user descriptions; the `fallback-users` is a list of user
descriptions that are used with usernames that don't appear in `users`.
- if `username` is present, then the entry only matches clients that
specify this exact username; otherwise, any username matches;
- if `password` is present, then the entry only matches clients that
specify this exact password; otherwise, any password matches.
Every user description is a dictionary with fields `password` and
`permissions`. The `password` field may be a literal password string, or
a dictionary describing a hashed password or a wildcard. The
`permissions` field should be one of `op`, `present` or `passive`. (An
array of Galene's internal permissions is also allowed, but this is not
recommended, since internal permissions may vary from version to version).
For example, the entry
For fexample, the entry
{"username": "jch", "password": "1234"}
"users": {"jch": {"password": "1234", "permissions": "op"}}
specifies username *jch* with password *1234*, while
specifies that user "jch" may login as operator with password "1234", while
{"password": "1234"}
"fallback-users": [{"password": "1234", "permissions": "present"}]
allows any username with password *1234*, and
allows any username with password *1234*. Finally,
{}
"fallback-users": [
{"password": {"type": "wildcard"}, "permissions": "present"}
]
allows any username with any password.
By default, empty usernames are forbidden; set the `allow-anonymous`
option to allow empty usernames. By default, recording is forbidden;
specify the `allow-recording` option to allow operators to record.
### Hashed passwords
@ -236,17 +247,20 @@ If you don't wish to store cleartext passwords on the server, you may
generate hashed passwords with the `galene-password-generator` utility. A
user entry with a hashed password looks like this:
{
"username": "jch",
"password": {
"type": "pbkdf2",
"hash": "sha-256",
"key": "f591c35604e6aef572851d9c3543c812566b032b6dc083c81edd15cc24449913",
"salt": "92bff2ace56fe38f",
"iterations": 4096
"users": {
"jch": {
"password": {
"type": "pbkdf2",
"hash": "sha-256",
"key": "f591c35604e6aef572851d9c3543c812566b032b6dc083c81edd15cc24449913",
"salt": "92bff2ace56fe38f",
"iterations": 4096
},
"permissions": "op"
}
}
### Stateful tokens
Stateful tokens allow to temporarily grant access to a user. In order to

View File

@ -24,8 +24,11 @@ func main() {
var length int
var saltLen int
var username string
var permissions string
flag.StringVar(&username, "user", "",
"generate entry for given `username`")
flag.StringVar(&permissions, "permissions", "present",
"`permissions` for user entry")
flag.StringVar(&algorithm, "hash", "pbkdf2",
"hashing `algorithm`")
flag.IntVar(&iterations, "iterations", 4096,
@ -82,9 +85,14 @@ func main() {
e := json.NewEncoder(os.Stdout)
if username != "" {
creds := group.ClientPattern{
Username: username,
Password: &p,
perms, err := group.NewPermissions(permissions)
if err != nil {
log.Fatalf("NewPermissions: %v", err)
}
creds := make(map[string]group.UserDescription)
creds[username] = group.UserDescription{
Password: p,
Permissions: perms,
}
err = e.Encode(creds)
} else {

View File

@ -27,7 +27,11 @@ type Password RawPassword
func (p Password) Match(pw string) (bool, error) {
switch p.Type {
case "":
return false, errors.New("missing password")
case "plain":
return p.Key == pw, nil
case "wildcard":
return true, nil
case "pbkdf2":
key, err := hex.DecodeString(p.Key)
if err != nil {
@ -64,6 +68,7 @@ func (p *Password) UnmarshalJSON(b []byte) error {
err := json.Unmarshal(b, &k)
if err == nil {
*p = Password{
Type: "plain",
Key: k,
}
return nil
@ -77,7 +82,7 @@ func (p *Password) UnmarshalJSON(b []byte) error {
}
func (p Password) MarshalJSON() ([]byte, error) {
if p.Type == "" && p.Hash == "" && p.Salt == "" && p.Iterations == 0 {
if p.Type == "plain" && p.Hash == "" && p.Salt == "" && p.Iterations == 0 {
return json.Marshal(p.Key)
}
return json.Marshal(RawPassword(p))

View File

@ -7,8 +7,13 @@ import (
"testing"
)
var pw1 = Password{}
var pw2 = Password{Key: "pass"}
var pw1 = Password{
Type: "plain",
}
var pw2 = Password{
Type: "plain",
Key: "pass",
}
var pw3 = Password{
Type: "pbkdf2",
Hash: "sha-256",
@ -20,11 +25,15 @@ var pw4 = Password{
Type: "bcrypt",
Key: "$2a$10$afOr2f33onT/nDFFyT3mbOq5FMSw1wWXfyTXQTBMbKvZpBkoD3Qwu",
}
var pw5 = Password{
var pw5 = Password{}
var pw6 = Password{
Type: "bad",
}
func TestGood(t *testing.T) {
if match, err := pw1.Match(""); err != nil || !match {
t.Errorf("pw2 doesn't match (%v)", err)
}
if match, err := pw2.Match("pass"); err != nil || !match {
t.Errorf("pw2 doesn't match (%v)", err)
}
@ -37,9 +46,6 @@ func TestGood(t *testing.T) {
}
func TestBad(t *testing.T) {
if match, err := pw1.Match("bad"); err != nil || match {
t.Errorf("pw1 matches")
}
if match, err := pw2.Match("bad"); err != nil || match {
t.Errorf("pw2 matches")
}
@ -49,8 +55,14 @@ func TestBad(t *testing.T) {
if match, err := pw4.Match("bad"); err != nil || match {
t.Errorf("pw4 matches")
}
if match, err := pw5.Match(""); err == nil || match {
t.Errorf("pw5 matches")
}
if match, err := pw5.Match("bad"); err == nil || match {
t.Errorf("pw4 matches")
t.Errorf("pw5 matches")
}
if match, err := pw6.Match("bad"); err == nil || match {
t.Errorf("pw6 matches")
}
}
@ -71,7 +83,7 @@ func TestJSON(t *testing.T) {
var pw2 Password
err = json.Unmarshal(j, &pw2)
if err != nil {
t.Fatalf("Unmarshal: %v", err)
t.Errorf("Unmarshal: %v", err)
} else if !reflect.DeepEqual(pw, pw2) {
t.Errorf("Expected %v, got %v", pw, pw2)
}

View File

@ -2,6 +2,8 @@ package group
import (
"encoding/json"
"errors"
"log"
"os"
"path"
"path/filepath"
@ -9,6 +11,106 @@ import (
"time"
)
type Permissions struct {
// non-empty for a named permissions set
name string
// only used when unnamed
permissions []string
}
var permissionsMap = map[string][]string{
"op": []string{"op", "present", "token"},
"present": []string{"present"},
"observe": []string{},
"admin": []string{"admin"},
}
func NewPermissions(name string) (Permissions, error) {
_, ok := permissionsMap[name]
if !ok {
return Permissions{}, errors.New("unknown permission")
}
return Permissions{
name: name,
}, nil
}
func (p Permissions) Permissions(desc *Description) []string {
if p.name == "" {
return p.permissions
}
perms := permissionsMap[p.name]
op := false
present := false
token := false
record := false
for _, p := range perms {
switch p {
case "op":
op = true
case "present":
present = true
case "token":
token = true
case "record":
record = true
}
}
if desc != nil && desc.AllowRecording {
if op && !record {
// copy the slice
perms = append([]string{"record"}, perms...)
}
}
if desc != nil && desc.UnrestrictedTokens {
if present && !token {
perms = append([]string{"token"}, perms...)
}
}
return perms
}
func (p *Permissions) UnmarshalJSON(b []byte) error {
var a []string
err := json.Unmarshal(b, &a)
if err == nil {
*p = Permissions{
permissions: a,
}
return nil
}
var s string
err = json.Unmarshal(b, &s)
if err == nil {
_, ok := permissionsMap[s]
if !ok {
return errors.New("Unknown permission " + s)
}
*p = Permissions{
name: s,
}
return nil
}
return err
}
func (p Permissions) MarshalJSON() ([]byte, error) {
if p.name != "" {
return json.Marshal(p.name)
}
return json.Marshal(p.permissions)
}
type UserDescription struct {
Password Password `json:"password"`
Permissions Permissions `json:"permissions"`
}
// Description represents a group description together with some metadata
// about the JSON file it was deserialised from.
type Description struct {
@ -47,14 +149,11 @@ type Description struct {
MaxHistoryAge int `json:"max-history-age,omitempty"`
// Time after which joining is no longer allowed
Expires *time.Time `json:"expires"`
Expires *time.Time `json:"expires,omitempty"`
// Time before which joining is not allowed
NotBefore *time.Time `json:"not-before,omitempty"`
// Whether users are allowed to log in with an empty username.
AllowAnonymous bool `json:"allow-anonymous,omitempty"`
// Whether recording is allowed.
AllowRecording bool `json:"allow-recording,omitempty"`
@ -62,7 +161,7 @@ type Description struct {
UnrestrictedTokens bool `json:"unrestricted-tokens,omitempty"`
// Whether subgroups are created on the fly.
AllowSubgroups bool `json:"allow-subgroups,omitempty"`
AutoSubgroups bool `json:"auto-subgroups,omitempty"`
// Whether to lock the group when the last op logs out.
Autolock bool `json:"autolock,omitempty"`
@ -70,14 +169,11 @@ type Description struct {
// Whether to kick all users when the last op logs out.
Autokick bool `json:"autokick,omitempty"`
// A list of logins for ops.
Op []ClientPattern `json:"op,omitempty"`
// Users allowed to login
Users map[string]UserDescription `json:"users,omitempty"`
// A list of logins for presenters.
Presenter []ClientPattern `json:"presenter,omitempty"`
// A list of logins for non-presenting users.
Other []ClientPattern `json:"other,omitempty"`
// Credentials for users with arbitrary username
FallbackUsers []UserDescription `json:"fallback-users,omitempty"`
// The (public) keys used for token authentication.
AuthKeys []map[string]interface{} `json:"authKeys,omitempty"`
@ -91,6 +187,13 @@ type Description struct {
// Codec preferences. If empty, a suitable default is chosen in
// the APIFromNames function.
Codecs []string `json:"codecs,omitempty"`
// Obsolete fields
Op []ClientPattern `json:"op,omitempty"`
Presenter []ClientPattern `json:"presenter,omitempty"`
Other []ClientPattern `json:"other,omitempty"`
AllowSubgroups bool `json:"allow-subgroups,omitempty"`
AllowAnonymous bool `json:"allow-anonymous,omitempty"`
}
const DefaultMaxHistoryAge = 4 * time.Hour
@ -181,7 +284,7 @@ func readDescription(name string) (*Description, error) {
return nil, err
}
if isParent {
if !desc.AllowSubgroups {
if !desc.AutoSubgroups {
return nil, os.ErrNotExist
}
desc.Public = false
@ -192,5 +295,78 @@ func readDescription(name string) (*Description, error) {
desc.fileSize = fi.Size()
desc.modTime = fi.ModTime()
err = upgradeDescription(&desc)
if err != nil {
return nil, err
}
return &desc, nil
}
func upgradeDescription(desc *Description) error {
if desc.AllowAnonymous {
log.Printf(
"%v: field allow-anonymous is obsolete, ignored",
desc.FileName,
)
desc.AllowAnonymous = false
}
if desc.AllowSubgroups {
desc.AutoSubgroups = true
desc.AllowSubgroups = false
}
upgradePassword := func(pw *Password) Password {
if pw == nil {
return Password{
Type: "wildcard",
}
}
return *pw
}
upgradeUser := func(u ClientPattern, p string) UserDescription {
return UserDescription{
Password: upgradePassword(u.Password),
Permissions: Permissions{
name: p,
},
}
}
upgradeUsers := func(ps []ClientPattern, p string) {
if desc.Users == nil {
desc.Users = make(map[string]UserDescription)
}
for _, u := range ps {
if u.Username == "" {
desc.FallbackUsers = append(desc.FallbackUsers,
upgradeUser(u, p))
continue
}
_, found := desc.Users[u.Username]
if found {
log.Printf("%v: duplicate user %v, ignored",
desc.FileName, u.Username)
continue
}
desc.Users[u.Username] = upgradeUser(u, p)
}
}
if desc.Op != nil {
upgradeUsers(desc.Op, "op")
desc.Op = nil
}
if desc.Presenter != nil {
upgradeUsers(desc.Presenter, "present")
desc.Presenter = nil
}
if desc.Other != nil {
upgradeUsers(desc.Other, "observe")
desc.Other = nil
}
return nil
}

97
group/description_test.go Normal file
View File

@ -0,0 +1,97 @@
package group
import (
"encoding/json"
"reflect"
"testing"
)
var descJSON = `
{
"max-history-age": 10,
"allow-subgroups": true,
"users": {
"jch": {"password": "topsecret", "permissions": "op"},
"john": {"password": "secret", "permissions": "present"},
"james": {"password": "secret2", "permissions": "observe"},
"peter": {"password": "secret4"}
},
"fallback-users": [
{"permissions": "observe", "password": {"type":"wildcard"}}
]
}`
func TestDescriptionJSON(t *testing.T) {
var d Description
err := json.Unmarshal([]byte(descJSON), &d)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
dd, err := json.Marshal(d)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var ddd Description
err = json.Unmarshal([]byte(dd), &ddd)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(d, ddd) {
t.Errorf("Got %v, expected %v", ddd, d)
}
}
var obsoleteJSON = `
{
"op": [{"username": "jch","password": "topsecret"}],
"max-history-age": 10,
"allow-subgroups": true,
"presenter": [
{"username": "john", "password": "secret"}
],
"other": [
{"username": "james", "password": "secret2"},
{"username": "peter", "password": "secret4"},
{}
]
}`
func TestUpgradeDescription(t *testing.T) {
var d1 Description
err := json.Unmarshal([]byte(descJSON), &d1)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
var d2 Description
err = json.Unmarshal([]byte(obsoleteJSON), &d2)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
err = upgradeDescription(&d2)
if err != nil {
t.Fatalf("upgradeDescription: %v", err)
}
if d2.Op != nil || d2.Presenter != nil || d2.Other != nil {
t.Errorf("legacy field is not nil")
}
if len(d1.Users) != len(d2.Users) {
t.Errorf("length not equal: %v != %v",
len(d1.Users), len(d2.Users))
}
for k, v1 := range d1.Users {
v2 := d2.Users[k]
if !reflect.DeepEqual(v1.Password, v2.Password) ||
!permissionsEqual(
v1.Permissions.Permissions(&d1),
v2.Permissions.Permissions(&d2),
) {
t.Errorf("%v not equal: %v != %v", k, v1, v2)
}
}
}

View File

@ -847,43 +847,6 @@ func (g *Group) GetChatHistory() []ChatHistoryEntry {
return h
}
func matchClient(creds ClientCredentials, users []ClientPattern) (bool, bool) {
if creds.Username == nil {
return false, false
}
username := *creds.Username
matched := false
for _, u := range users {
if u.Username == username {
matched = true
if u.Password == nil {
return true, true
}
m, _ := u.Password.Match(creds.Password)
if m {
return true, true
}
}
}
if matched {
return true, false
}
for _, u := range users {
if u.Username == "" {
if u.Password == nil {
return true, true
}
m, _ := u.Password.Match(creds.Password)
if m {
return true, true
}
}
}
return false, false
}
// Configuration represents the contents of the data/config.json file.
type Configuration struct {
// The modtime and size of the file. These are used to detect
@ -891,10 +854,13 @@ type Configuration struct {
modTime time.Time `json:"-"`
fileSize int64 `json:"-"`
PublicServer bool `json:"publicServer"`
CanonicalHost string `json:"canonicalHost"`
ProxyURL string `json:"proxyURL"`
Admin []ClientPattern `json:"admin"`
PublicServer bool `json:"publicServer"`
CanonicalHost string `json:"canonicalHost"`
ProxyURL string `json:"proxyURL"`
Users map[string]UserDescription
// obsolete fields
Admin []ClientPattern `json:"admin"`
}
func (conf Configuration) Zero() bool {
@ -945,52 +911,56 @@ func GetConfiguration() (*Configuration, error) {
if err != nil {
return nil, err
}
if conf.Admin != nil {
log.Printf("%v: field \"admin\" is obsolete, ignored", filename)
conf.Admin = nil
}
configuration.configuration = &conf
return configuration.configuration, nil
}
// called locked
func (g *Group) getPasswordPermission(creds ClientCredentials) ([]string, error) {
func (g *Group) getPasswordPermission(creds ClientCredentials) (Permissions, error) {
desc := g.description
if creds.Username == nil {
return nil, errors.New("username not provided")
return Permissions{}, errors.New("username not provided")
}
if !desc.AllowAnonymous && *creds.Username == "" {
return nil, ErrAnonymousNotAuthorised
}
if found, good := matchClient(creds, desc.Op); found {
if good {
p := []string{"op", "present", "token"}
if desc.AllowRecording {
p = append(p, "record")
if desc.Users != nil {
if c, found := desc.Users[*creds.Username]; found {
ok, err := c.Password.Match(creds.Password)
if err != nil {
return Permissions{}, err
}
return p, nil
}
return nil, &NotAuthorisedError{}
}
if found, good := matchClient(creds, desc.Presenter); found {
if good {
p := []string{"present"}
if desc.UnrestrictedTokens {
p = append(p, "token")
if ok {
return c.Permissions, nil
} else {
return Permissions{}, &NotAuthorisedError{}
}
return p, nil
}
return nil, &NotAuthorisedError{}
}
if found, good := matchClient(creds, desc.Other); found {
if good {
p := []string{}
if desc.UnrestrictedTokens {
p = append(p, "token")
}
return p, nil
}
return nil, &NotAuthorisedError{}
for _, c := range desc.FallbackUsers {
if c.Password.Type == "wildcard" {
continue
}
ok, _ := c.Password.Match(creds.Password)
if ok {
return c.Permissions, nil
}
}
return nil, &NotAuthorisedError{}
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 true if there is a user entry with the given username.
@ -1003,21 +973,12 @@ func (g *Group) UserExists(username string) bool {
// called locked
func (g *Group) userExists(username string) bool {
if username == "" {
desc := g.description
if desc.Users == nil {
return false
}
desc := g.description
for _, ps := range [][]ClientPattern{
desc.Op, desc.Presenter, desc.Other,
} {
for _, p := range ps {
if p.Username == username {
return true
}
}
}
return false
_, found := desc.Users[username]
return found
}
// called locked
@ -1049,11 +1010,11 @@ func (g *Group) getPermission(creds ClientCredentials) (string, []string, error)
}
} else if creds.Username != nil {
username = *creds.Username
var err error
perms, err = g.getPasswordPermission(creds)
ps, err := g.getPasswordPermission(creds)
if err != nil {
return "", nil, err
}
perms = ps.Permissions(desc)
} else {
return "", nil, errors.New("neither username nor token provided")
}

View File

@ -4,9 +4,9 @@ import (
"encoding/json"
"errors"
"fmt"
"reflect"
"testing"
"time"
"sort"
)
func TestGroup(t *testing.T) {
@ -70,50 +70,35 @@ func TestChatHistory(t *testing.T) {
}
}
var descJSON = `
{
"op": [{"username": "jch","password": "topsecret"}],
"max-history-age": 10,
"allow-subgroups": true,
"presenter": [
{"username": "john", "password": "secret"},
{"username": "john", "password": "secret2"}
],
"other": [
{"username": "james", "password": "secret3"},
{"username": "peter", "password": "secret4"},
{}
]
}`
func TestDescriptionJSON(t *testing.T) {
var d Description
err := json.Unmarshal([]byte(descJSON), &d)
if err != nil {
t.Fatalf("unmarshal: %v", err)
func permissionsEqual(a, b []string) bool {
// nil case
if len(a) == 0 && len(b) == 0 {
return true
}
dd, err := json.Marshal(d)
if err != nil {
t.Fatalf("marshal: %v", err)
if len(a) != len(b) {
return false
}
var ddd Description
err = json.Unmarshal([]byte(dd), &ddd)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(d, ddd) {
t.Errorf("Got %v, expected %v", ddd, d)
aa := append([]string(nil), a...)
sort.Slice(aa, func(i, j int) bool {
return aa[i] < aa[j]
})
bb := append([]string(nil), b...)
sort.Slice(bb, func(i, j int) bool {
return bb[i] < bb[j]
})
for i := range aa {
if aa[i] != bb[i] {
return false
}
}
return true
}
var jch = "jch"
var john = "john"
var james = "james"
var paul = "paul"
var peter = "peter"
var badClients = []ClientCredentials{
{Username: &jch, Password: "foo"},
@ -136,17 +121,17 @@ var goodClients = []credPerm{
[]string{"present"},
},
{
ClientCredentials{Username: &john, Password: "secret2"},
[]string{"present"},
},
{
ClientCredentials{Username: &james, Password: "secret3"},
ClientCredentials{Username: &james, Password: "secret2"},
[]string{},
},
{
ClientCredentials{Username: &paul, Password: "secret3"},
[]string{},
},
{
ClientCredentials{Username: &peter, Password: "secret4"},
[]string{},
},
}
func TestPermissions(t *testing.T) {
@ -172,7 +157,7 @@ func TestPermissions(t *testing.T) {
if err != nil {
t.Errorf("GetPermission %v: %v", cp.c, err)
} else if u != *cp.c.Username ||
!reflect.DeepEqual(p, cp.p) {
!permissionsEqual(p, cp.p) {
t.Errorf("%v: got %v %v, expected %v",
cp.c, u, p, cp.p)
}
@ -181,6 +166,55 @@ func TestPermissions(t *testing.T) {
}
func TestExtraPermissions(t *testing.T) {
j := `
{
"users": {
"jch": {"password": "topsecret", "permissions": "op"},
"john": {"password": "secret", "permissions": "present"},
"james": {"password": "secret2", "permissions": "observe"}
}
}`
var d Description
err := json.Unmarshal([]byte(j), &d)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
doit := func(u string, p []string) {
pu := d.Users[u].Permissions.Permissions(&d)
if !permissionsEqual(pu, p) {
t.Errorf("%v: expected %v, got %v", u, p, pu)
}
}
doit("jch", []string{"op", "token", "present"})
doit("john", []string{"present"})
doit("james", []string{})
d.AllowRecording = true
d.UnrestrictedTokens = false
doit("jch", []string{"op", "record", "token", "present"})
doit("john", []string{"present"})
doit("james", []string{})
d.AllowRecording = false
d.UnrestrictedTokens = true
doit("jch", []string{"op", "token", "present"})
doit("john", []string{"token", "present"})
doit("james", []string{})
d.AllowRecording = true
d.UnrestrictedTokens = true
doit("jch", []string{"op", "record", "token", "present"})
doit("john", []string{"token", "present"})
doit("james", []string{})
}
func TestUsernameTaken(t *testing.T) {
var g Group
err := json.Unmarshal([]byte(descJSON), &g.description)

View File

@ -466,13 +466,24 @@ func adminMatch(username, password string) (bool, error) {
return false, err
}
for _, cred := range conf.Admin {
if cred.Username == "" || cred.Username == username {
if ok, _ := cred.Password.Match(password); ok {
u, found := conf.Users[username]
if found {
ok, err := u.Password.Match(password)
if err != nil {
return false, err
}
if !ok {
return false, nil
}
perms := u.Permissions.Permissions(nil)
for _, p := range perms {
if p == "admin" {
return true, nil
}
}
return false, nil
}
return false, nil
}

View File

@ -8,8 +8,9 @@ import (
"path/filepath"
"testing"
"github.com/jech/galene/group"
"github.com/pion/webrtc/v3"
"github.com/jech/galene/group"
)
func TestParseGroupName(t *testing.T) {
@ -190,6 +191,54 @@ func TestFormatICEServer(t *testing.T) {
}
}
func TestMatchAdmin(t *testing.T) {
d := t.TempDir()
group.DataDirectory = d
filename := filepath.Join(d, "config.json")
f, err := os.Create(filename)
if err != nil {
t.Fatalf("Create %v: %v", filename, err)
}
f.Write([]byte(`{
"users": {
"root": {"password": "pwd", "permissions": "admin"},
"notroot": {"password": "pwd"}
}
}`))
f.Close()
ok, err := adminMatch("jch", "pwd")
if ok || err != nil {
t.Errorf("jch: %v %v", ok, err)
}
ok, err = adminMatch("root", "pwd")
if !ok || err != nil {
t.Errorf("root: %v %v", ok, err)
}
ok, err = adminMatch("root", "notpwd")
if ok || err != nil {
t.Errorf("root: %v %v", ok, err)
}
ok, err = adminMatch("root", "")
if ok || err != nil {
t.Errorf("root: %v %v", ok, err)
}
ok, err = adminMatch("notroot", "pwd")
if ok || err != nil {
t.Errorf("notroot: %v %v", ok, err)
}
ok, err = adminMatch("notroot", "notpwd")
if ok || err != nil {
t.Errorf("notroot: %v %v", ok, err)
}
}
func TestObfuscate(t *testing.T) {
id := newId()
obfuscated, err := obfuscate(id)