Skip to content

Commit

Permalink
Merge pull request #897 from semi-technologies/extend-admin-list-with…
Browse files Browse the repository at this point in the history
…-read-only-list

Extend admin list with read only list
  • Loading branch information
etiennedi committed Jun 24, 2019
2 parents 1c47df7 + bdf9141 commit b36a089
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 73 deletions.
2 changes: 1 addition & 1 deletion adapters/handlers/rest/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Decentralised Knowledge Graph
https
Host: localhost
BasePath: /weaviate/v1
Version: 0.14.6
Version: 0.15.0
Contact: Weaviate<hello@semi.technology> https://github.com/semi-technologies
Consumes:
Expand Down
4 changes: 2 additions & 2 deletions adapters/handlers/rest/embedded_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func init() {
"url": "https://github.com/semi-technologies",
"email": "hello@semi.technology"
},
"version": "0.14.6"
"version": "0.15.0"
},
"basePath": "/weaviate/v1",
"paths": {
Expand Down Expand Up @@ -3157,7 +3157,7 @@ func init() {
"url": "https://github.com/semi-technologies",
"email": "hello@semi.technology"
},
"version": "0.14.6"
"version": "0.15.0"
},
"basePath": "/weaviate/v1",
"paths": {
Expand Down
24 changes: 19 additions & 5 deletions docs/en/use/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ If the user is contained on this list, they get all permissions. If they aren't
they get none. It's not possible to assign only some rights to a specific user
with the Admin List plugin.

### Read-Only list (introduced in 0.15.0)

Version 0.15.0 adds a new functionality to the Admin List plugin: Other than a
list of admins, it is now also possible to specify a list of read-only users.
Those users have permissions on all `get` and `list` operations, but no other
permissions.

If a subject is present on both the admin and read-only list, weaviate will
error on startup due to the invalid configuration.

### Usage

Simply configure the admin plugin in the config yaml like so:
Expand All @@ -31,18 +41,22 @@ authorization:
users:
- jane@doe.com
- john@doe.com
read_only_users: # only available from 0.15.0 on, ignored in prior versions
- roberta@doe.com # only available from 0.15.0 on, ignored in prior versions
```

The above would enable the plugin and make users `jane@doe.com` and
`john@doe.com` admins. Note that in the above example email ids are used to
`john@doe.com` admins. If at least weaviate version `0.15.0` is used,
additionally user `roberta@doe.com` will have read-only permissions.

Note that in the above example email ids are used to
identify the user. This is not a requirement, in fact, any string can be used.
This depends on what you configured in the authentication settings. For
example, if you are using Open ID Connect authentication, you could set the
`authentication.oidc.username_claim` to `email` to achieve the result shown
above.

## RBAC
Full Role-Based Access Control (RBAC) coming soon. As of now any authenticated
user is fully authorized to read, write, modify or delete any resource.


More fine-grained Role-Based Access Control (RBAC) coming soon. As of know the
only possible distinction is between Admins (CRUD), Read-Only Users and
entirely unauthorized users.
2 changes: 1 addition & 1 deletion openapi-specs/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@
},
"description": "Decentralised Knowledge Graph",
"title": "Weaviate - Decentralised Knowledge Graph",
"version": "0.14.6"
"version": "0.15.0"
},
"parameters": {
"CommonLimitParameterQuery": {
Expand Down
3 changes: 2 additions & 1 deletion tools/dev/config.local-oidc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ authorization:
admin_list:
enabled: true
users:
- differentjohn@doe.com
read_only_users:
- john@doe.com
analytics_engine:
enabled: true
defaultUseAnalyticsEngine: false
Expand Down
36 changes: 27 additions & 9 deletions usecases/auth/authorization/adminlist/authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,56 @@
import (
"github.com/semi-technologies/weaviate/entities/models"
"github.com/semi-technologies/weaviate/usecases/auth/authorization/errors"
"github.com/semi-technologies/weaviate/usecases/config"
)

// Authorizer provides either full (admin) or no access
type Authorizer struct {
allowedUsers map[string]int
adminUsers map[string]int
readOnlyUsers map[string]int
}

// New Authorizer using the AdminList method
func New(cfg config.AdminList) *Authorizer {
func New(cfg Config) *Authorizer {
a := &Authorizer{}
a.addUserList(cfg.Users)
a.addAdminUserList(cfg.Users)
a.addReadOnlyUserList(cfg.ReadOnlyUsers)
return a
}

// Authorize will give full access (to any resource!) if the user is part of
// the admin list or no access at all if they are not
func (a *Authorizer) Authorize(principal *models.Principal, verb, resource string) error {
if _, ok := a.allowedUsers[principal.Username]; ok {
if _, ok := a.adminUsers[principal.Username]; ok {
return nil
}

if verb == "get" || verb == "list" {
if _, ok := a.readOnlyUsers[principal.Username]; ok {
return nil
}
}

return errors.NewForbidden(principal, verb, resource)
}

func (a *Authorizer) addUserList(users []string) {
func (a *Authorizer) addAdminUserList(users []string) {
// build a map for more effecient lookup on long lists
if a.adminUsers == nil {
a.adminUsers = map[string]int{}
}

for _, user := range users {
a.adminUsers[user] = 1
}
}

func (a *Authorizer) addReadOnlyUserList(users []string) {
// build a map for more effecient lookup on long lists
if a.allowedUsers == nil {
a.allowedUsers = map[string]int{}
if a.readOnlyUsers == nil {
a.readOnlyUsers = map[string]int{}
}

for _, user := range users {
a.allowedUsers[user] = 1
a.readOnlyUsers[user] = 1
}
}
177 changes: 132 additions & 45 deletions usecases/auth/authorization/adminlist/authorizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,57 +15,144 @@ import (

"github.com/semi-technologies/weaviate/entities/models"
"github.com/semi-technologies/weaviate/usecases/auth/authorization/errors"
"github.com/semi-technologies/weaviate/usecases/config"
"github.com/stretchr/testify/assert"
)

func Test_AdminList_Authorizor(t *testing.T) {
t.Run("with no users configured at all", func(t *testing.T) {
cfg := config.AdminList{
Enabled: true,
Users: []string{},
}

principal := &models.Principal{
Username: "johndoe",
}

err := New(cfg).Authorize(principal, "get", "things")
assert.Equal(t, errors.NewForbidden(principal, "get", "things"), err,
"should have the correct err msg")
})
t.Run("with read requests", func(t *testing.T) {
t.Run("with no users configured at all", func(t *testing.T) {
cfg := Config{
Enabled: true,
Users: []string{},
}

principal := &models.Principal{
Username: "johndoe",
}

err := New(cfg).Authorize(principal, "get", "things")
assert.Equal(t, errors.NewForbidden(principal, "get", "things"), err,
"should have the correct err msg")
})

t.Run("with a non-configured user, it denies the request", func(t *testing.T) {
cfg := Config{
Enabled: true,
Users: []string{
"alice",
},
}

principal := &models.Principal{
Username: "johndoe",
}

err := New(cfg).Authorize(principal, "get", "things")
assert.Equal(t, errors.NewForbidden(principal, "get", "things"), err,
"should have the correct err msg")
})

t.Run("with a configured admin user, it allows the request", func(t *testing.T) {
cfg := Config{
Enabled: true,
Users: []string{
"alice",
"johndoe",
},
}

principal := &models.Principal{
Username: "johndoe",
}

err := New(cfg).Authorize(principal, "get", "things")
assert.Nil(t, err)
})

t.Run("with a configured read-only user, it allows the request", func(t *testing.T) {
cfg := Config{
Enabled: true,
ReadOnlyUsers: []string{
"alice",
"johndoe",
},
}

principal := &models.Principal{
Username: "johndoe",
}

t.Run("with a non-configured user, it denies the request", func(t *testing.T) {
cfg := config.AdminList{
Enabled: true,
Users: []string{
"alice",
},
}

principal := &models.Principal{
Username: "johndoe",
}

err := New(cfg).Authorize(principal, "get", "things")
assert.Equal(t, errors.NewForbidden(principal, "get", "things"), err,
"should have the correct err msg")
err := New(cfg).Authorize(principal, "get", "things")
assert.Nil(t, err)
})
})

t.Run("with a configured user, it allows the request", func(t *testing.T) {
cfg := config.AdminList{
Enabled: true,
Users: []string{
"alice",
"johndoe",
},
}

principal := &models.Principal{
Username: "johndoe",
}

err := New(cfg).Authorize(principal, "get", "things")
assert.Nil(t, err)
t.Run("with write/delete requests", func(t *testing.T) {
t.Run("with no users configured at all", func(t *testing.T) {
cfg := Config{
Enabled: true,
Users: []string{},
}

principal := &models.Principal{
Username: "johndoe",
}

err := New(cfg).Authorize(principal, "create", "things")
assert.Equal(t, errors.NewForbidden(principal, "create", "things"), err,
"should have the correct err msg")
})

t.Run("with a non-configured user, it denies the request", func(t *testing.T) {
cfg := Config{
Enabled: true,
Users: []string{
"alice",
},
}

principal := &models.Principal{
Username: "johndoe",
}

err := New(cfg).Authorize(principal, "create", "things")
assert.Equal(t, errors.NewForbidden(principal, "create", "things"), err,
"should have the correct err msg")
})

t.Run("with a configured admin user, it allows the request", func(t *testing.T) {
cfg := Config{
Enabled: true,
Users: []string{
"alice",
"johndoe",
},
}

principal := &models.Principal{
Username: "johndoe",
}

err := New(cfg).Authorize(principal, "create", "things")
assert.Nil(t, err)
})

t.Run("with a configured read-only user, it denies the request", func(t *testing.T) {
cfg := Config{
Enabled: true,
ReadOnlyUsers: []string{
"alice",
"johndoe",
},
}

principal := &models.Principal{
Username: "johndoe",
}

err := New(cfg).Authorize(principal, "create", "things")
assert.Equal(t, errors.NewForbidden(principal, "create", "things"), err,
"should have the correct err msg")
})
})
}
43 changes: 43 additions & 0 deletions usecases/auth/authorization/adminlist/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* _ _
*__ _____ __ ___ ___ __ _| |_ ___
*\ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
* \ V V / __/ (_| |\ V /| | (_| | || __/
* \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
*
* Copyright © 2016 - 2019 Weaviate. All rights reserved.
* LICENSE: https://github.com/semi-technologies/weaviate/blob/develop/LICENSE.md
* DESIGN & CONCEPT: Bob van Luijt (@bobvanluijt)
* CONTACT: hello@semi.technology
*/package adminlist

import "fmt"

// Config makes every subject on the list an admin, whereas everyone else
// has no rights whatsoever
type Config struct {
Enabled bool `json:"enabled" yaml:"enabled"`
Users []string `json:"users" yaml:"users"`
ReadOnlyUsers []string `json:"read_only_users" yaml:"read_only_users"`
}

// Validate admin list config for viability, can be called from the central
// config package
func (c Config) Validate() error {
return c.validateOverlap()
}

// we are expecting both lists to always contain few subjects and know that
// this comparison is only done once (at startup). We are therefore fine with
// the O(n^2) complexity of this very primitive overlap search in favor of very
// simple code.
func (c Config) validateOverlap() error {
for _, a := range c.Users {
for _, b := range c.ReadOnlyUsers {
if a == b {
return fmt.Errorf("admin list: subject '%s' is present on both admin and read-only list", a)
}
}
}

return nil
}
Loading

0 comments on commit b36a089

Please sign in to comment.