mirror of
https://github.com/jech/galene.git
synced 2024-11-09 18:25:58 +01:00
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:
parent
eb54f2a9bb
commit
d887a216f0
10 changed files with 571 additions and 204 deletions
94
README
94
README
|
@ -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",
|
||||
"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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
97
group/description_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
127
group/group.go
127
group/group.go
|
@ -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
|
||||
|
@ -894,6 +857,9 @@ type Configuration struct {
|
|||
PublicServer bool `json:"publicServer"`
|
||||
CanonicalHost string `json:"canonicalHost"`
|
||||
ProxyURL string `json:"proxyURL"`
|
||||
Users map[string]UserDescription
|
||||
|
||||
// obsolete fields
|
||||
Admin []ClientPattern `json:"admin"`
|
||||
}
|
||||
|
||||
|
@ -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 desc.Users != nil {
|
||||
if c, found := desc.Users[*creds.Username]; found {
|
||||
ok, err := c.Password.Match(creds.Password)
|
||||
if err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
if found, good := matchClient(creds, desc.Op); found {
|
||||
if good {
|
||||
p := []string{"op", "present", "token"}
|
||||
if desc.AllowRecording {
|
||||
p = append(p, "record")
|
||||
if ok {
|
||||
return c.Permissions, nil
|
||||
} else {
|
||||
return Permissions{}, &NotAuthorisedError{}
|
||||
}
|
||||
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")
|
||||
}
|
||||
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
|
||||
}
|
||||
return nil, &NotAuthorisedError{}
|
||||
ok, _ := c.Password.Match(creds.Password)
|
||||
if ok {
|
||||
return c.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 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 == "" {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
if desc.Users == nil {
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
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
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(d, ddd) {
|
||||
t.Errorf("Got %v, expected %v", ddd, d)
|
||||
}
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue