upd(messaging): permission API with DB
This commit is contained in:
parent
ea71c581f3
commit
654201aa73
@ -718,12 +718,6 @@
|
||||
}
|
||||
],
|
||||
"get": [
|
||||
{
|
||||
"name": "scope",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"title": "Permissions scope"
|
||||
},
|
||||
{
|
||||
"name": "resource",
|
||||
"type": "string",
|
||||
@ -756,6 +750,22 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "scopes",
|
||||
"path": "/permissions/scopes/{scope}",
|
||||
"method": "GET",
|
||||
"title": "List resources for given scope",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "scope",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"title": "Scope"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -30,12 +30,6 @@
|
||||
"Path": "/permissions/{teamID}",
|
||||
"Parameters": {
|
||||
"get": [
|
||||
{
|
||||
"name": "scope",
|
||||
"required": true,
|
||||
"title": "Permissions scope",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "resource",
|
||||
"required": true,
|
||||
@ -76,6 +70,22 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "scopes",
|
||||
"Method": "GET",
|
||||
"Title": "List resources for given scope",
|
||||
"Path": "/permissions/scopes/{scope}",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "scope",
|
||||
"required": true,
|
||||
"title": "Scope",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -475,7 +475,6 @@ The following event types may be sent with a message event:
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| scope | string | GET | Permissions scope | N/A | YES |
|
||||
| resource | string | GET | Permissions resource | N/A | YES |
|
||||
| teamID | uint64 | PATH | Team ID | N/A | YES |
|
||||
|
||||
@ -494,6 +493,20 @@ The following event types may be sent with a message event:
|
||||
| teamID | uint64 | PATH | Team ID | N/A | YES |
|
||||
| permissions | []rules.Rules | POST | List of rules to set | N/A | YES |
|
||||
|
||||
## List resources for given scope
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/permissions/permissions/scopes/{scope}` | HTTP/S | GET | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| scope | string | PATH | Scope | N/A | YES |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -6,10 +6,14 @@ import (
|
||||
"github.com/crusttech/crust/system/types"
|
||||
)
|
||||
|
||||
func Crust() types.Organisation {
|
||||
return types.Organisation{ID: 1}
|
||||
}
|
||||
|
||||
func GetFromContext(ctx context.Context) types.Organisation {
|
||||
if orgID, ok := ctx.Value("organizationID").(uint64); ok {
|
||||
return types.Organisation{ID: orgID}
|
||||
} else {
|
||||
return types.Organisation{ID: 1}
|
||||
return Crust()
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,13 +30,15 @@ type PermissionsAPI interface {
|
||||
List(context.Context, *request.PermissionsList) (interface{}, error)
|
||||
Get(context.Context, *request.PermissionsGet) (interface{}, error)
|
||||
Set(context.Context, *request.PermissionsSet) (interface{}, error)
|
||||
Scopes(context.Context, *request.PermissionsScopes) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
type Permissions struct {
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Get func(http.ResponseWriter, *http.Request)
|
||||
Set func(http.ResponseWriter, *http.Request)
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Get func(http.ResponseWriter, *http.Request)
|
||||
Set func(http.ResponseWriter, *http.Request)
|
||||
Scopes func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewPermissions(ph PermissionsAPI) *Permissions {
|
||||
@ -62,6 +64,13 @@ func NewPermissions(ph PermissionsAPI) *Permissions {
|
||||
return ph.Set(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Scopes: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewPermissionsScopes()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return ph.Scopes(r.Context(), params)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,6 +81,7 @@ func (ph *Permissions) MountRoutes(r chi.Router, middlewares ...func(http.Handle
|
||||
r.Get("/permissions", ph.List)
|
||||
r.Get("/permissions/{teamID}", ph.Get)
|
||||
r.Post("/permissions/{teamID}", ph.Set)
|
||||
r.Get("/permissions/scopes/{scope}", ph.Scopes)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -25,9 +25,13 @@ func (ctrl *Permissions) List(ctx context.Context, r *request.PermissionsList) (
|
||||
}
|
||||
|
||||
func (ctrl *Permissions) Get(ctx context.Context, r *request.PermissionsGet) (interface{}, error) {
|
||||
return ctrl.svc.perms.Get(r.TeamID, r.Scope, r.Resource)
|
||||
return ctrl.svc.perms.Get(r.TeamID, r.Resource)
|
||||
}
|
||||
|
||||
func (ctrl *Permissions) SetTeam(ctx context.Context, r *request.PermissionsSet) (interface{}, error) {
|
||||
func (ctrl *Permissions) Set(ctx context.Context, r *request.PermissionsSet) (interface{}, error) {
|
||||
return ctrl.svc.perms.Set(r.TeamID, r.Permissions)
|
||||
}
|
||||
|
||||
func (ctrl *Permissions) Scopes(ctx context.Context, r *request.PermissionsScopes) (interface{}, error) {
|
||||
return ctrl.svc.perms.Scopes(r.Scope)
|
||||
}
|
||||
|
||||
@ -73,7 +73,6 @@ var _ RequestFiller = NewPermissionsList()
|
||||
|
||||
// Permissions get request parameters
|
||||
type PermissionsGet struct {
|
||||
Scope string
|
||||
Resource string
|
||||
TeamID uint64 `json:",string"`
|
||||
}
|
||||
@ -109,10 +108,6 @@ func (p *PermissionsGet) Fill(r *http.Request) (err error) {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := get["scope"]; ok {
|
||||
|
||||
p.Scope = val
|
||||
}
|
||||
if val, ok := get["resource"]; ok {
|
||||
|
||||
p.Resource = val
|
||||
@ -167,3 +162,46 @@ func (p *PermissionsSet) Fill(r *http.Request) (err error) {
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewPermissionsSet()
|
||||
|
||||
// Permissions scopes request parameters
|
||||
type PermissionsScopes struct {
|
||||
Scope string
|
||||
}
|
||||
|
||||
func NewPermissionsScopes() *PermissionsScopes {
|
||||
return &PermissionsScopes{}
|
||||
}
|
||||
|
||||
func (p *PermissionsScopes) Fill(r *http.Request) (err error) {
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(p)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := r.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := r.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
p.Scope = chi.URLParam(r, "scope")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewPermissionsScopes()
|
||||
|
||||
@ -22,6 +22,7 @@ func MountRoutes() func(chi.Router) {
|
||||
handlers.NewChannel(Channel{}.New()).MountRoutes(r)
|
||||
handlers.NewMessage(Message{}.New()).MountRoutes(r)
|
||||
handlers.NewSearch(Search{}.New()).MountRoutes(r)
|
||||
handlers.NewPermissions(Permissions{}.New()).MountRoutes(r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,14 +2,64 @@ package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/namsral/flag"
|
||||
"github.com/titpetric/factory"
|
||||
|
||||
systemMigrate "github.com/crusttech/crust/system/db"
|
||||
)
|
||||
|
||||
type mockDB struct{}
|
||||
|
||||
func (mockDB) Transaction(callback func() error) error { return callback() }
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// @todo this is a very optimistic initialization, make it more robust
|
||||
godotenv.Load("../../.env")
|
||||
|
||||
prefix := "messaging"
|
||||
dsn := ""
|
||||
|
||||
p := func(s string) string {
|
||||
return prefix + "-" + s
|
||||
}
|
||||
|
||||
flag.StringVar(&dsn, p("db-dsn"), "crust:crust@tcp(db1:3306)/crust?collation=utf8mb4_general_ci", "DSN for database connection")
|
||||
flag.Parse()
|
||||
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
|
||||
factory.Database.Add("default", dsn)
|
||||
|
||||
db := factory.Database.MustGet()
|
||||
db.Profiler = &factory.Database.ProfilerStdout
|
||||
|
||||
// migrate database schema
|
||||
if err := systemMigrate.Migrate(db); err != nil {
|
||||
log.Printf("Error running migrations: %+v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// clean up tables
|
||||
{
|
||||
for _, name := range []string{"sys_user", "sys_team", "sys_team_member", "sys_organisation", "sys_rules"} {
|
||||
_, err := db.Exec("truncate " + name)
|
||||
if err != nil {
|
||||
panic("Error when clearing " + name + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func assert(t *testing.T, ok bool, format string, args ...interface{}) bool {
|
||||
if !ok {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
|
||||
@ -5,8 +5,11 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/crusttech/crust/internal/organization"
|
||||
internalRules "github.com/crusttech/crust/internal/rules"
|
||||
"github.com/crusttech/crust/messaging/repository"
|
||||
"github.com/crusttech/crust/messaging/types"
|
||||
systemRepository "github.com/crusttech/crust/system/repository"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -14,15 +17,21 @@ type (
|
||||
db db
|
||||
ctx context.Context
|
||||
|
||||
scopes internalRules.ScopeInterface
|
||||
team systemRepository.TeamRepository
|
||||
channel repository.ChannelRepository
|
||||
|
||||
scopes internalRules.ScopeInterface
|
||||
resources internalRules.ResourcesInterface
|
||||
}
|
||||
|
||||
PermissionsService interface {
|
||||
With(ctx context.Context) PermissionsService
|
||||
|
||||
List() (interface{}, error)
|
||||
Get(teamID uint64, scope string, resource string) (interface{}, error)
|
||||
Get(teamID uint64, resource string) (interface{}, error)
|
||||
Set(teamID uint64, rules []internalRules.Rules) (interface{}, error)
|
||||
|
||||
Scopes(scope string) (interface{}, error)
|
||||
}
|
||||
)
|
||||
|
||||
@ -32,12 +41,17 @@ func Permissions(scopes internalRules.ScopeInterface) PermissionsService {
|
||||
}).With(context.Background())
|
||||
}
|
||||
|
||||
func (svc *permissions) With(ctx context.Context) PermissionsService {
|
||||
func (p *permissions) With(ctx context.Context) PermissionsService {
|
||||
db := repository.DB(ctx)
|
||||
return &permissions{
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
scopes: svc.scopes,
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
|
||||
team: systemRepository.Team(ctx, db),
|
||||
channel: repository.Channel(ctx, db),
|
||||
|
||||
scopes: p.scopes,
|
||||
resources: internalRules.NewResources(ctx, db),
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,10 +59,41 @@ func (p *permissions) List() (interface{}, error) {
|
||||
return p.scopes.List(), nil
|
||||
}
|
||||
|
||||
func (p *permissions) Get(teamID uint64, scope string, resource string) (interface{}, error) {
|
||||
return nil, errors.New("service.permissions.get: not implemented")
|
||||
func (p *permissions) Get(teamID uint64, resource string) (interface{}, error) {
|
||||
return p.resources.ListGrants(teamID, resource)
|
||||
}
|
||||
|
||||
func (p *permissions) Set(teamID uint64, rules []internalRules.Rules) (interface{}, error) {
|
||||
return nil, errors.New("service.permissions.set: not implemented")
|
||||
var err error
|
||||
for _, rule := range rules {
|
||||
err = p.resources.Grant(
|
||||
teamID,
|
||||
rule.Resource,
|
||||
[]string{rule.Operation},
|
||||
rule.Value,
|
||||
)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (p *permissions) Scopes(scope string) (interface{}, error) {
|
||||
switch scope {
|
||||
case "organization":
|
||||
// @todo organizations from DB once multi-org
|
||||
// return p.organizaion.Find(nil)
|
||||
orgs := []types.Organisation{
|
||||
types.Organisation{
|
||||
organization.Crust(),
|
||||
},
|
||||
}
|
||||
return orgs, nil
|
||||
case "team":
|
||||
return p.team.Find(nil)
|
||||
case "channel":
|
||||
return p.channel.FindChannels(nil)
|
||||
}
|
||||
return nil, errors.New("no scope defined")
|
||||
}
|
||||
|
||||
119
messaging/service/permissions_test.go
Normal file
119
messaging/service/permissions_test.go
Normal file
@ -0,0 +1,119 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/titpetric/factory"
|
||||
|
||||
"github.com/crusttech/crust/internal/auth"
|
||||
internalRules "github.com/crusttech/crust/internal/rules"
|
||||
. "github.com/crusttech/crust/internal/test"
|
||||
"github.com/crusttech/crust/messaging/types"
|
||||
systemRepos "github.com/crusttech/crust/system/repository"
|
||||
systemTypes "github.com/crusttech/crust/system/types"
|
||||
)
|
||||
|
||||
func TestPermissions(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
return
|
||||
}
|
||||
ctx := context.TODO()
|
||||
|
||||
// Create user for test.
|
||||
userRepo := systemRepos.User(ctx, factory.Database.MustGet())
|
||||
user := &systemTypes.User{
|
||||
Name: "John Doe",
|
||||
Username: "johndoe",
|
||||
SatosaID: "1234",
|
||||
}
|
||||
err := user.GeneratePassword("johndoe")
|
||||
NoError(t, err, "expected no error generating password, got %v", err)
|
||||
|
||||
_, err = userRepo.Create(user)
|
||||
NoError(t, err, "expected no error creating user, got %v", err)
|
||||
|
||||
// Create team for test and add user
|
||||
teamRepo := systemRepos.Team(ctx, factory.Database.MustGet())
|
||||
team := &systemTypes.Team{
|
||||
Name: "Test team v1",
|
||||
}
|
||||
_, err = teamRepo.Create(team)
|
||||
NoError(t, err, "expected no error creating team, got %v", err)
|
||||
|
||||
err = teamRepo.MemberAddByID(team.ID, user.ID)
|
||||
NoError(t, err, "expected no error adding user to team, got %v", err)
|
||||
|
||||
// Set Identity.
|
||||
ctx = auth.SetIdentityToContext(ctx, user)
|
||||
|
||||
// Create scopes.
|
||||
scopes := internalRules.NewScope()
|
||||
scopes.Add(&types.Organisation{})
|
||||
scopes.Add(&types.Team{})
|
||||
scopes.Add(&types.Channel{})
|
||||
|
||||
permissionsSvc := Permissions(scopes).With(ctx)
|
||||
|
||||
// Get all available scopes and items
|
||||
list, err := permissionsSvc.List()
|
||||
|
||||
scopeItems := list.([]internalRules.ScopeItem)
|
||||
NoError(t, err, "expected no error, receiving scopes")
|
||||
Assert(t, len(scopeItems) == 3, "expected 3 scopes, got %v", len(scopeItems))
|
||||
|
||||
// Setup nothing for organization
|
||||
organisationScope := scopeItems[0]
|
||||
Assert(t, organisationScope.Scope == "organisation", "expected scope 'organisation', got %s", organisationScope.Scope)
|
||||
|
||||
// Setup everything allow for team
|
||||
teamScope := scopeItems[1]
|
||||
Assert(t, teamScope.Scope == "team", "expected scope 'team', got %s", teamScope.Scope)
|
||||
|
||||
rules := make([]internalRules.Rules, 0)
|
||||
for _, group := range teamScope.Permissions {
|
||||
for _, op := range group.Operations {
|
||||
r := internalRules.Rules{
|
||||
TeamID: team.ID,
|
||||
Resource: "team:1",
|
||||
Operation: op.Key,
|
||||
Value: internalRules.Allow,
|
||||
}
|
||||
rules = append(rules, r)
|
||||
}
|
||||
}
|
||||
_, err = permissionsSvc.Set(team.ID, rules)
|
||||
NoError(t, err, "expected no error, setting rules")
|
||||
|
||||
// Deny all permissions for scope channel:1
|
||||
channelScope := scopeItems[2]
|
||||
Assert(t, channelScope.Scope == "channel", "expected scope 'channel', got %s", channelScope.Scope)
|
||||
|
||||
rules = make([]internalRules.Rules, 0)
|
||||
for _, group := range channelScope.Permissions {
|
||||
for _, op := range group.Operations {
|
||||
r := internalRules.Rules{
|
||||
TeamID: team.ID,
|
||||
Resource: "channel:1",
|
||||
Operation: op.Key,
|
||||
Value: internalRules.Deny,
|
||||
}
|
||||
rules = append(rules, r)
|
||||
}
|
||||
}
|
||||
_, err = permissionsSvc.Set(team.ID, rules)
|
||||
NoError(t, err, "expected no error, setting rules")
|
||||
|
||||
// Check permission on channel and team level to test inheritance.
|
||||
rls := Rules().With(ctx)
|
||||
|
||||
canManageChannels := rls.canManageChannels()
|
||||
Assert(t, canManageChannels == false, "expected canManageChannels == false, got %v", canManageChannels)
|
||||
|
||||
canSendMessage := rls.canSendMessages(&types.Channel{ID: 1})
|
||||
Assert(t, canSendMessage == false, "expected canSendMessage == false, got %v", canSendMessage)
|
||||
|
||||
canSendMessage = rls.canSendMessages(&types.Channel{ID: 2})
|
||||
Assert(t, canSendMessage == true, "expected canSendMessage == true, got %v", canSendMessage)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user