mirror of
https://github.com/jech/galene.git
synced 2024-11-22 08:35:57 +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:
|
file may look as follows:
|
||||||
|
|
||||||
{
|
{
|
||||||
"admin":[{"username":"root","password":"secret"}],
|
"users":{"root": {"password":"secret", "permissions": "admin"}},
|
||||||
"canonicalHost": "galene.example.org"
|
"canonicalHost": "galene.example.org"
|
||||||
}
|
}
|
||||||
|
|
||||||
The fields are as follows:
|
The fields are as follows:
|
||||||
|
|
||||||
- `admin` defines the users allowed to look at the `/stats.html` file; it
|
- `users` defines the users allowed to administer the server, and has the
|
||||||
has the same syntax as user definitions in groups (see below).
|
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
|
- `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
|
allowed. This is safe if the server is on the public Internet, but not
|
||||||
necessarily so if it is on a private network.
|
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:
|
A typical group definition file looks like this:
|
||||||
|
|
||||||
{
|
{
|
||||||
"op":[{"username":"jch","password":"1234"}],
|
"users":{
|
||||||
|
"jch": {"password":"1234", "permissions": "op"}
|
||||||
|
},
|
||||||
"allow-recording": true,
|
"allow-recording": true,
|
||||||
"allow-subgroups": true
|
"auto-subgroups": true
|
||||||
}
|
}
|
||||||
|
|
||||||
This defines a group with the operator (administrator) username *jch* and
|
This defines a group with the operator (administrator) username *jch* and
|
||||||
password *1234*. The `allow-recording` entry says that the operator is
|
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
|
that subgroups will be created automatically. This particular group does
|
||||||
not allow password login for ordinary users, and is suitable if you use
|
not allow password login for ordinary users, and is suitable if you use
|
||||||
invitations (see *Stateful Tokens* below) for ordinary users.
|
invitations (see *Stateful Tokens* below) for ordinary users.
|
||||||
|
|
||||||
In order to allow password login for ordinary users, add a list of users
|
In order to allow password login for ordinary users, add password entries
|
||||||
as the entry `presenter`:
|
with the permission `present`:
|
||||||
|
|
||||||
{
|
{
|
||||||
"op": [{"username":"jch","password":"1234"}],
|
"users":{
|
||||||
"presenter": [{"username":"john", "password": "secret"}]
|
"jch": {"password":"1234", "permissions": "op"}
|
||||||
|
"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 and an empty password:
|
username using the `fallback-users` entry::
|
||||||
|
|
||||||
{
|
{
|
||||||
"op": [{"username":"jch","password":"1234"}],
|
"users":{
|
||||||
"presenter": [{}],
|
"jch": {"password":"1234", "permissions": "op"}
|
||||||
|
},
|
||||||
|
"fallback-users": [
|
||||||
|
{"password": {"type": "wildcard"}, "permissions": "present"}
|
||||||
|
],
|
||||||
"public": true
|
"public": true
|
||||||
}
|
}
|
||||||
|
|
||||||
The empty dictionary `{}` is a wildcard entry: it matches any username and
|
The password `{"type": "wildcard"}` indicates that any password will be
|
||||||
any password. Setting `public` causes the group to be displayed in the
|
accepted.
|
||||||
list of public groups on the landing page
|
|
||||||
|
|
||||||
|
|
||||||
## Reference
|
## 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`),
|
specify at least one user definition (`op`, `presenter`, or `other`),
|
||||||
nobody will be able to join the group. The following fields are allowed:
|
nobody will be able to join the group. The following fields are allowed:
|
||||||
|
|
||||||
- `op`, `presenter`, `other`: each of these is an array of user
|
- `users`: is a dictionary that maps user names to dictionaries with
|
||||||
definitions (see *Authorisation* below) and specifies the users allowed
|
entries `password` and `permissions`; `permissions` should be one of
|
||||||
to connect respectively with operator privileges, with presenter
|
`op`, `present` or `passive`;
|
||||||
privileges, and as passive listeners;
|
- `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;
|
- `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;
|
||||||
|
@ -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"
|
- `unrestricted-tokens`: if true, then ordinary users (without the "op"
|
||||||
privilege) are allowed to create tokens;
|
privilege) are allowed to create tokens;
|
||||||
- `allow-anonymous`: if true, then users may connect with an empty username;
|
- `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;
|
are automatically created when first accessed;
|
||||||
- `autolock`: if true, the group will start locked and become locked
|
- `autolock`: if true, the group will start locked and become locked
|
||||||
whenever there are no clients with operator privileges;
|
whenever there are no clients with operator privileges;
|
||||||
|
@ -202,33 +212,34 @@ even Unix passwords).
|
||||||
### Password authorisation
|
### Password authorisation
|
||||||
|
|
||||||
When password authorisation is used, authorised usernames and password are
|
When password authorisation is used, authorised usernames and password are
|
||||||
defined directly in the group configuration file, in the `op`, `presenter`
|
defined directly in the group configuration file, in the `users` and
|
||||||
and `other` arrays. Each member of the array is a dictionary, that may
|
`fallback-users` entries. The `users` entry is a dictionary that maps
|
||||||
contain the fields `username` and `password`:
|
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
|
Every user description is a dictionary with fields `password` and
|
||||||
specify this exact username; otherwise, any username matches;
|
`permissions`. The `password` field may be a literal password string, or
|
||||||
- if `password` is present, then the entry only matches clients that
|
a dictionary describing a hashed password or a wildcard. The
|
||||||
specify this exact password; otherwise, any password matches.
|
`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.
|
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
|
### 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
|
generate hashed passwords with the `galene-password-generator` utility. A
|
||||||
user entry with a hashed password looks like this:
|
user entry with a hashed password looks like this:
|
||||||
|
|
||||||
{
|
"users": {
|
||||||
"username": "jch",
|
"jch": {
|
||||||
"password": {
|
"password": {
|
||||||
"type": "pbkdf2",
|
"type": "pbkdf2",
|
||||||
"hash": "sha-256",
|
"hash": "sha-256",
|
||||||
"key": "f591c35604e6aef572851d9c3543c812566b032b6dc083c81edd15cc24449913",
|
"key": "f591c35604e6aef572851d9c3543c812566b032b6dc083c81edd15cc24449913",
|
||||||
"salt": "92bff2ace56fe38f",
|
"salt": "92bff2ace56fe38f",
|
||||||
"iterations": 4096
|
"iterations": 4096
|
||||||
|
},
|
||||||
|
"permissions": "op"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
### Stateful tokens
|
### Stateful tokens
|
||||||
|
|
||||||
Stateful tokens allow to temporarily grant access to a user. In order to
|
Stateful tokens allow to temporarily grant access to a user. In order to
|
||||||
|
|
|
@ -24,8 +24,11 @@ func main() {
|
||||||
var length int
|
var length int
|
||||||
var saltLen int
|
var saltLen int
|
||||||
var username string
|
var username string
|
||||||
|
var permissions string
|
||||||
flag.StringVar(&username, "user", "",
|
flag.StringVar(&username, "user", "",
|
||||||
"generate entry for given `username`")
|
"generate entry for given `username`")
|
||||||
|
flag.StringVar(&permissions, "permissions", "present",
|
||||||
|
"`permissions` for user entry")
|
||||||
flag.StringVar(&algorithm, "hash", "pbkdf2",
|
flag.StringVar(&algorithm, "hash", "pbkdf2",
|
||||||
"hashing `algorithm`")
|
"hashing `algorithm`")
|
||||||
flag.IntVar(&iterations, "iterations", 4096,
|
flag.IntVar(&iterations, "iterations", 4096,
|
||||||
|
@ -82,9 +85,14 @@ func main() {
|
||||||
|
|
||||||
e := json.NewEncoder(os.Stdout)
|
e := json.NewEncoder(os.Stdout)
|
||||||
if username != "" {
|
if username != "" {
|
||||||
creds := group.ClientPattern{
|
perms, err := group.NewPermissions(permissions)
|
||||||
Username: username,
|
if err != nil {
|
||||||
Password: &p,
|
log.Fatalf("NewPermissions: %v", err)
|
||||||
|
}
|
||||||
|
creds := make(map[string]group.UserDescription)
|
||||||
|
creds[username] = group.UserDescription{
|
||||||
|
Password: p,
|
||||||
|
Permissions: perms,
|
||||||
}
|
}
|
||||||
err = e.Encode(creds)
|
err = e.Encode(creds)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -27,7 +27,11 @@ type Password RawPassword
|
||||||
func (p Password) Match(pw string) (bool, error) {
|
func (p Password) Match(pw string) (bool, error) {
|
||||||
switch p.Type {
|
switch p.Type {
|
||||||
case "":
|
case "":
|
||||||
|
return false, errors.New("missing password")
|
||||||
|
case "plain":
|
||||||
return p.Key == pw, nil
|
return p.Key == pw, nil
|
||||||
|
case "wildcard":
|
||||||
|
return true, nil
|
||||||
case "pbkdf2":
|
case "pbkdf2":
|
||||||
key, err := hex.DecodeString(p.Key)
|
key, err := hex.DecodeString(p.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -64,6 +68,7 @@ func (p *Password) UnmarshalJSON(b []byte) error {
|
||||||
err := json.Unmarshal(b, &k)
|
err := json.Unmarshal(b, &k)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
*p = Password{
|
*p = Password{
|
||||||
|
Type: "plain",
|
||||||
Key: k,
|
Key: k,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -77,7 +82,7 @@ func (p *Password) UnmarshalJSON(b []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Password) MarshalJSON() ([]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(p.Key)
|
||||||
}
|
}
|
||||||
return json.Marshal(RawPassword(p))
|
return json.Marshal(RawPassword(p))
|
||||||
|
|
|
@ -7,8 +7,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pw1 = Password{}
|
var pw1 = Password{
|
||||||
var pw2 = Password{Key: "pass"}
|
Type: "plain",
|
||||||
|
}
|
||||||
|
var pw2 = Password{
|
||||||
|
Type: "plain",
|
||||||
|
Key: "pass",
|
||||||
|
}
|
||||||
var pw3 = Password{
|
var pw3 = Password{
|
||||||
Type: "pbkdf2",
|
Type: "pbkdf2",
|
||||||
Hash: "sha-256",
|
Hash: "sha-256",
|
||||||
|
@ -20,11 +25,15 @@ var pw4 = Password{
|
||||||
Type: "bcrypt",
|
Type: "bcrypt",
|
||||||
Key: "$2a$10$afOr2f33onT/nDFFyT3mbOq5FMSw1wWXfyTXQTBMbKvZpBkoD3Qwu",
|
Key: "$2a$10$afOr2f33onT/nDFFyT3mbOq5FMSw1wWXfyTXQTBMbKvZpBkoD3Qwu",
|
||||||
}
|
}
|
||||||
var pw5 = Password{
|
var pw5 = Password{}
|
||||||
|
var pw6 = Password{
|
||||||
Type: "bad",
|
Type: "bad",
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGood(t *testing.T) {
|
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 {
|
if match, err := pw2.Match("pass"); err != nil || !match {
|
||||||
t.Errorf("pw2 doesn't match (%v)", err)
|
t.Errorf("pw2 doesn't match (%v)", err)
|
||||||
}
|
}
|
||||||
|
@ -37,9 +46,6 @@ func TestGood(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBad(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 {
|
if match, err := pw2.Match("bad"); err != nil || match {
|
||||||
t.Errorf("pw2 matches")
|
t.Errorf("pw2 matches")
|
||||||
}
|
}
|
||||||
|
@ -49,8 +55,14 @@ func TestBad(t *testing.T) {
|
||||||
if match, err := pw4.Match("bad"); err != nil || match {
|
if match, err := pw4.Match("bad"); err != nil || match {
|
||||||
t.Errorf("pw4 matches")
|
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 {
|
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
|
var pw2 Password
|
||||||
err = json.Unmarshal(j, &pw2)
|
err = json.Unmarshal(j, &pw2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unmarshal: %v", err)
|
t.Errorf("Unmarshal: %v", err)
|
||||||
} else if !reflect.DeepEqual(pw, pw2) {
|
} else if !reflect.DeepEqual(pw, pw2) {
|
||||||
t.Errorf("Expected %v, got %v", pw, pw2)
|
t.Errorf("Expected %v, got %v", pw, pw2)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package group
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -9,6 +11,106 @@ import (
|
||||||
"time"
|
"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
|
// Description represents a group description together with some metadata
|
||||||
// about the JSON file it was deserialised from.
|
// about the JSON file it was deserialised from.
|
||||||
type Description struct {
|
type Description struct {
|
||||||
|
@ -47,14 +149,11 @@ type Description struct {
|
||||||
MaxHistoryAge int `json:"max-history-age,omitempty"`
|
MaxHistoryAge int `json:"max-history-age,omitempty"`
|
||||||
|
|
||||||
// Time after which joining is no longer allowed
|
// 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
|
// Time before which joining is not allowed
|
||||||
NotBefore *time.Time `json:"not-before,omitempty"`
|
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.
|
// Whether recording is allowed.
|
||||||
AllowRecording bool `json:"allow-recording,omitempty"`
|
AllowRecording bool `json:"allow-recording,omitempty"`
|
||||||
|
|
||||||
|
@ -62,7 +161,7 @@ type Description struct {
|
||||||
UnrestrictedTokens bool `json:"unrestricted-tokens,omitempty"`
|
UnrestrictedTokens bool `json:"unrestricted-tokens,omitempty"`
|
||||||
|
|
||||||
// Whether subgroups are created on the fly.
|
// 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.
|
// Whether to lock the group when the last op logs out.
|
||||||
Autolock bool `json:"autolock,omitempty"`
|
Autolock bool `json:"autolock,omitempty"`
|
||||||
|
@ -70,14 +169,11 @@ type Description struct {
|
||||||
// Whether to kick all users when the last op logs out.
|
// Whether to kick all users when the last op logs out.
|
||||||
Autokick bool `json:"autokick,omitempty"`
|
Autokick bool `json:"autokick,omitempty"`
|
||||||
|
|
||||||
// A list of logins for ops.
|
// Users allowed to login
|
||||||
Op []ClientPattern `json:"op,omitempty"`
|
Users map[string]UserDescription `json:"users,omitempty"`
|
||||||
|
|
||||||
// A list of logins for presenters.
|
// Credentials for users with arbitrary username
|
||||||
Presenter []ClientPattern `json:"presenter,omitempty"`
|
FallbackUsers []UserDescription `json:"fallback-users,omitempty"`
|
||||||
|
|
||||||
// A list of logins for non-presenting users.
|
|
||||||
Other []ClientPattern `json:"other,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"`
|
||||||
|
@ -91,6 +187,13 @@ type Description struct {
|
||||||
// Codec preferences. If empty, a suitable default is chosen in
|
// Codec preferences. If empty, a suitable default is chosen in
|
||||||
// the APIFromNames function.
|
// the APIFromNames function.
|
||||||
Codecs []string `json:"codecs,omitempty"`
|
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
|
const DefaultMaxHistoryAge = 4 * time.Hour
|
||||||
|
@ -181,7 +284,7 @@ func readDescription(name string) (*Description, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if isParent {
|
if isParent {
|
||||||
if !desc.AllowSubgroups {
|
if !desc.AutoSubgroups {
|
||||||
return nil, os.ErrNotExist
|
return nil, os.ErrNotExist
|
||||||
}
|
}
|
||||||
desc.Public = false
|
desc.Public = false
|
||||||
|
@ -192,5 +295,78 @@ func readDescription(name string) (*Description, error) {
|
||||||
desc.fileSize = fi.Size()
|
desc.fileSize = fi.Size()
|
||||||
desc.modTime = fi.ModTime()
|
desc.modTime = fi.ModTime()
|
||||||
|
|
||||||
|
err = upgradeDescription(&desc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &desc, nil
|
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
|
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.
|
// Configuration represents the contents of the data/config.json file.
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
// The modtime and size of the file. These are used to detect
|
// The modtime and size of the file. These are used to detect
|
||||||
|
@ -894,6 +857,9 @@ type Configuration struct {
|
||||||
PublicServer bool `json:"publicServer"`
|
PublicServer bool `json:"publicServer"`
|
||||||
CanonicalHost string `json:"canonicalHost"`
|
CanonicalHost string `json:"canonicalHost"`
|
||||||
ProxyURL string `json:"proxyURL"`
|
ProxyURL string `json:"proxyURL"`
|
||||||
|
Users map[string]UserDescription
|
||||||
|
|
||||||
|
// obsolete fields
|
||||||
Admin []ClientPattern `json:"admin"`
|
Admin []ClientPattern `json:"admin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -945,52 +911,56 @@ func GetConfiguration() (*Configuration, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if conf.Admin != nil {
|
||||||
|
log.Printf("%v: field \"admin\" is obsolete, ignored", filename)
|
||||||
|
conf.Admin = nil
|
||||||
|
}
|
||||||
configuration.configuration = &conf
|
configuration.configuration = &conf
|
||||||
return configuration.configuration, nil
|
return configuration.configuration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// called locked
|
// called locked
|
||||||
func (g *Group) getPasswordPermission(creds ClientCredentials) ([]string, error) {
|
func (g *Group) getPasswordPermission(creds ClientCredentials) (Permissions, error) {
|
||||||
desc := g.description
|
desc := g.description
|
||||||
|
|
||||||
if creds.Username == nil {
|
if creds.Username == nil {
|
||||||
return nil, errors.New("username not provided")
|
return Permissions{}, errors.New("username not provided")
|
||||||
}
|
}
|
||||||
if !desc.AllowAnonymous && *creds.Username == "" {
|
if desc.Users != nil {
|
||||||
return nil, ErrAnonymousNotAuthorised
|
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 ok {
|
||||||
if good {
|
return c.Permissions, nil
|
||||||
p := []string{"op", "present", "token"}
|
} else {
|
||||||
if desc.AllowRecording {
|
return Permissions{}, &NotAuthorisedError{}
|
||||||
p = append(p, "record")
|
|
||||||
}
|
}
|
||||||
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.
|
// Return true if there is a user entry with the given username.
|
||||||
|
@ -1003,22 +973,13 @@ func (g *Group) UserExists(username string) bool {
|
||||||
|
|
||||||
// called locked
|
// called locked
|
||||||
func (g *Group) userExists(username string) bool {
|
func (g *Group) userExists(username string) bool {
|
||||||
if username == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
desc := g.description
|
desc := g.description
|
||||||
for _, ps := range [][]ClientPattern{
|
if desc.Users == nil {
|
||||||
desc.Op, desc.Presenter, desc.Other,
|
|
||||||
} {
|
|
||||||
for _, p := range ps {
|
|
||||||
if p.Username == username {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
_, found := desc.Users[username]
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
// called locked
|
// called locked
|
||||||
func (g *Group) getPermission(creds ClientCredentials) (string, []string, error) {
|
func (g *Group) getPermission(creds ClientCredentials) (string, []string, error) {
|
||||||
|
@ -1049,11 +1010,11 @@ func (g *Group) getPermission(creds ClientCredentials) (string, []string, error)
|
||||||
}
|
}
|
||||||
} else if creds.Username != nil {
|
} else if creds.Username != nil {
|
||||||
username = *creds.Username
|
username = *creds.Username
|
||||||
var err error
|
ps, err := g.getPasswordPermission(creds)
|
||||||
perms, err = g.getPasswordPermission(creds)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
perms = ps.Permissions(desc)
|
||||||
} else {
|
} else {
|
||||||
return "", nil, errors.New("neither username nor token provided")
|
return "", nil, errors.New("neither username nor token provided")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGroup(t *testing.T) {
|
func TestGroup(t *testing.T) {
|
||||||
|
@ -70,50 +70,35 @@ func TestChatHistory(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var descJSON = `
|
func permissionsEqual(a, b []string) bool {
|
||||||
{
|
// nil case
|
||||||
"op": [{"username": "jch","password": "topsecret"}],
|
if len(a) == 0 && len(b) == 0 {
|
||||||
"max-history-age": 10,
|
return true
|
||||||
"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)
|
|
||||||
}
|
}
|
||||||
|
if len(a) != len(b) {
|
||||||
dd, err := json.Marshal(d)
|
return false
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal: %v", err)
|
|
||||||
}
|
}
|
||||||
|
aa := append([]string(nil), a...)
|
||||||
var ddd Description
|
sort.Slice(aa, func(i, j int) bool {
|
||||||
err = json.Unmarshal([]byte(dd), &ddd)
|
return aa[i] < aa[j]
|
||||||
if err != nil {
|
})
|
||||||
t.Fatalf("unmarshal: %v", err)
|
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 jch = "jch"
|
||||||
var john = "john"
|
var john = "john"
|
||||||
var james = "james"
|
var james = "james"
|
||||||
var paul = "paul"
|
var paul = "paul"
|
||||||
|
var peter = "peter"
|
||||||
|
|
||||||
var badClients = []ClientCredentials{
|
var badClients = []ClientCredentials{
|
||||||
{Username: &jch, Password: "foo"},
|
{Username: &jch, Password: "foo"},
|
||||||
|
@ -136,17 +121,17 @@ var goodClients = []credPerm{
|
||||||
[]string{"present"},
|
[]string{"present"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ClientCredentials{Username: &john, Password: "secret2"},
|
ClientCredentials{Username: &james, Password: "secret2"},
|
||||||
[]string{"present"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ClientCredentials{Username: &james, Password: "secret3"},
|
|
||||||
[]string{},
|
[]string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ClientCredentials{Username: &paul, Password: "secret3"},
|
ClientCredentials{Username: &paul, Password: "secret3"},
|
||||||
[]string{},
|
[]string{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ClientCredentials{Username: &peter, Password: "secret4"},
|
||||||
|
[]string{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPermissions(t *testing.T) {
|
func TestPermissions(t *testing.T) {
|
||||||
|
@ -172,7 +157,7 @@ func TestPermissions(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("GetPermission %v: %v", cp.c, err)
|
t.Errorf("GetPermission %v: %v", cp.c, err)
|
||||||
} else if u != *cp.c.Username ||
|
} else if u != *cp.c.Username ||
|
||||||
!reflect.DeepEqual(p, cp.p) {
|
!permissionsEqual(p, cp.p) {
|
||||||
t.Errorf("%v: got %v %v, expected %v",
|
t.Errorf("%v: got %v %v, expected %v",
|
||||||
cp.c, u, p, cp.p)
|
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) {
|
func TestUsernameTaken(t *testing.T) {
|
||||||
var g Group
|
var g Group
|
||||||
err := json.Unmarshal([]byte(descJSON), &g.description)
|
err := json.Unmarshal([]byte(descJSON), &g.description)
|
||||||
|
|
|
@ -466,13 +466,24 @@ func adminMatch(username, password string) (bool, error) {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cred := range conf.Admin {
|
u, found := conf.Users[username]
|
||||||
if cred.Username == "" || cred.Username == username {
|
if found {
|
||||||
if ok, _ := cred.Password.Match(password); ok {
|
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 true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,9 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/jech/galene/group"
|
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
|
|
||||||
|
"github.com/jech/galene/group"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseGroupName(t *testing.T) {
|
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) {
|
func TestObfuscate(t *testing.T) {
|
||||||
id := newId()
|
id := newId()
|
||||||
obfuscated, err := obfuscate(id)
|
obfuscated, err := obfuscate(id)
|
||||||
|
|
Loading…
Reference in a new issue