3
0

upd(messaging): permission API with DB

This commit is contained in:
Mitja Zivkovic 2019-02-15 17:34:11 +01:00
parent ea71c581f3
commit 654201aa73
11 changed files with 337 additions and 33 deletions

View File

@ -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"
}
]
}
}
]
}

View File

@ -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"
}
]
}
}
]
}

View File

@ -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 |

View File

@ -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()
}
}

View File

@ -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)
})
})
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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)
})
}
}

View File

@ -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)

View File

@ -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")
}

View 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)
}