3
0

upd(messaging): add permission checking

This commit is contained in:
Mitja Zivkovic
2019-02-24 10:06:16 +01:00
parent 397278a8d8
commit 2ecc83c52e
16 changed files with 329 additions and 196 deletions

View File

@@ -9,7 +9,7 @@ import (
type ResourcesInterface interface {
With(ctx context.Context, db *factory.DB) ResourcesInterface
IsAllowed(resource string, operation string) Access
Check(resource string, operation string) Access
Grant(roleID uint64, rules []Rule) error
Read(roleID uint64) ([]Rule, error)

View File

@@ -35,7 +35,7 @@ func (r *resources) identity() uint64 {
// IsAllowed function checks granted permission for specific resource and operation. Permission checks on
// global level are not allowed and will always return Deny.
func (r *resources) IsAllowed(resource string, operation string) Access {
func (r *resources) Check(resource string, operation string) Access {
parts := strings.Split(resource, delimiter)
// Permission check on global level is not allowed.

View File

@@ -40,8 +40,8 @@ func TestRules(t *testing.T) {
// default (unset=deny), forbidden check ...:*
{
Expect(rules.Inherit, resources.IsAllowed("messaging:channel:1", "update"), "messaging:channel:1 update - Inherit")
Expect(rules.Deny, resources.IsAllowed("messaging:channel:*", "update"), "messaging:channel:* update - Deny")
Expect(rules.Inherit, resources.Check("messaging:channel:1", "update"), "messaging:channel:1 update - Inherit")
Expect(rules.Deny, resources.Check("messaging:channel:*", "update"), "messaging:channel:* update - Deny")
}
// allow messaging:channel:2 update,delete
@@ -53,9 +53,9 @@ func TestRules(t *testing.T) {
err := resources.Grant(roleID, list)
NoError(t, err, "expect no error")
Expect(rules.Inherit, resources.IsAllowed("messaging:channel:1", "update"), "messaging:channel:1 update - Inherit")
Expect(rules.Allow, resources.IsAllowed("messaging:channel:2", "update"), "messaging:channel:2 update - Allow")
Expect(rules.Deny, resources.IsAllowed("messaging:channel:*", "update"), "messaging:channel:* update - Deny")
Expect(rules.Inherit, resources.Check("messaging:channel:1", "update"), "messaging:channel:1 update - Inherit")
Expect(rules.Allow, resources.Check("messaging:channel:2", "update"), "messaging:channel:2 update - Allow")
Expect(rules.Deny, resources.Check("messaging:channel:*", "update"), "messaging:channel:* update - Deny")
}
// list grants for test role
@@ -79,9 +79,9 @@ func TestRules(t *testing.T) {
err := resources.Grant(roleID, list)
NoError(t, err, "expect no error")
Expect(rules.Deny, resources.IsAllowed("messaging:channel:1", "update"), "messaging:channel:1 update - Deny")
Expect(rules.Allow, resources.IsAllowed("messaging:channel:2", "update"), "messaging:channel:2 update - Allow")
Expect(rules.Deny, resources.IsAllowed("messaging:channel:*", "update"), "messaging:channel:* update - Deny")
Expect(rules.Deny, resources.Check("messaging:channel:1", "update"), "messaging:channel:1 update - Deny")
Expect(rules.Allow, resources.Check("messaging:channel:2", "update"), "messaging:channel:2 update - Allow")
Expect(rules.Deny, resources.Check("messaging:channel:*", "update"), "messaging:channel:* update - Deny")
}
// reset messaging:channel:1, messaging:channel:2
@@ -95,8 +95,8 @@ func TestRules(t *testing.T) {
err := resources.Grant(roleID, list)
NoError(t, err, "expect no error")
Expect(rules.Inherit, resources.IsAllowed("messaging:channel:1", "update"), "messaging:channel:1 update - Inherit")
Expect(rules.Inherit, resources.IsAllowed("messaging:channel:2", "update"), "messaging:channel:2 update - Inherit")
Expect(rules.Inherit, resources.Check("messaging:channel:1", "update"), "messaging:channel:1 update - Inherit")
Expect(rules.Inherit, resources.Check("messaging:channel:2", "update"), "messaging:channel:2 update - Inherit")
}
// [messaging:channel:*,update] - allow, [messaging:channel:1, deny]
@@ -110,8 +110,8 @@ func TestRules(t *testing.T) {
err := resources.Grant(roleID, list)
NoError(t, err, "expected no error")
Expect(rules.Deny, resources.IsAllowed("messaging:channel:1", "update"), "messaging:channel:1 update - Deny")
Expect(rules.Allow, resources.IsAllowed("messaging:channel:2", "update"), "messaging:channel:2 update - Allow")
Expect(rules.Deny, resources.Check("messaging:channel:1", "update"), "messaging:channel:1 update - Deny")
Expect(rules.Allow, resources.Check("messaging:channel:2", "update"), "messaging:channel:2 update - Allow")
}
// list all by roleID

View File

@@ -8,7 +8,7 @@ import (
)
func MountRoutes() func(chi.Router) {
// Initialize handers & controllers.
// Initialize handlers & controllers.
return func(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(auth.MiddlewareValidOnly404)

View File

@@ -8,6 +8,7 @@ import (
"github.com/pkg/errors"
"github.com/crusttech/crust/internal/auth"
"github.com/crusttech/crust/internal/organization"
"github.com/crusttech/crust/messaging/repository"
"github.com/crusttech/crust/messaging/types"
systemService "github.com/crusttech/crust/system/service"
@@ -19,7 +20,9 @@ type (
ctx context.Context
usr systemService.UserService
evl EventService
prm PermissionsService
channel repository.ChannelRepository
cmember repository.ChannelMemberRepository
@@ -52,10 +55,11 @@ type (
Undelete(ID uint64) (*types.Channel, error)
RecordView(userID, channelID, lastMessageID uint64) error
}
)
// channelSecurity interface {
// CanRead(ch *types.Channel) bool
// }
var (
ErrUnknownChannelType = errors.New("Unknown ChannelType")
ErrNoPermission = errors.New("No permissions")
)
const (
@@ -67,6 +71,7 @@ func Channel() ChannelService {
return (&channel{
usr: systemService.DefaultUser,
evl: DefaultEvent,
prm: DefaultPermissions,
}).With(context.Background())
}
@@ -78,6 +83,7 @@ func (svc *channel) With(ctx context.Context) ChannelService {
usr: svc.usr.With(ctx),
evl: svc.evl.With(ctx),
prm: svc.prm.With(ctx),
channel: repository.Channel(ctx, db),
cmember: repository.ChannelMember(ctx, db),
@@ -197,13 +203,10 @@ func (svc *channel) FindMembers(channelID uint64) (out types.ChannelMemberSet, e
}
func (svc *channel) Create(in *types.Channel) (out *types.Channel, err error) {
// @todo: [SECURITY] permission check if user can add channel
return out, svc.db.Transaction(func() (err error) {
var msg *types.Message
// @todo get organisation from somewhere
var organisationID uint64 = 0
var organisationID = organization.Crust().ID
var chCreatorID = repository.Identity(svc.ctx)
@@ -220,23 +223,19 @@ func (svc *channel) Create(in *types.Channel) (out *types.Channel, err error) {
}
}
// @todo [SECURITY] check if channel topic can be set
if in.Topic != "" && false {
if in.Topic != "" && svc.prm.CanUpdate(in) {
return errors.New("Not allowed to set channel topic")
}
// @todo [SECURITY] check if user can create public channels
if in.Type == types.ChannelTypePublic && false {
if in.Type == types.ChannelTypePublic && svc.prm.CanCreatePublicChannel() {
return errors.New("Not allowed to create public channels")
}
// @todo [SECURITY] check if user can create private channels
if in.Type == types.ChannelTypePrivate && false {
return errors.New("Not allowed to create public channels")
if in.Type == types.ChannelTypePrivate && svc.prm.CanCreatePrivateChannel() {
return errors.New("Not allowed to create private channels")
}
// @todo [SECURITY] check if user can create private channels
if in.Type == types.ChannelTypeGroup && false {
if in.Type == types.ChannelTypeGroup && svc.prm.CanCreateDirectChannel() {
return errors.New("Not allowed to create group channels")
}

View File

@@ -0,0 +1,158 @@
package service
import (
"context"
internalRules "github.com/crusttech/crust/internal/rules"
"github.com/crusttech/crust/messaging/repository"
"github.com/crusttech/crust/messaging/types"
systemService "github.com/crusttech/crust/system/service"
)
type (
permissions struct {
db db
ctx context.Context
prm systemService.PermissionsService
}
// Fallback option
Fallback func() bool
PermissionsService interface {
With(context.Context) PermissionsService
CanAccessMessaging() bool
CanGrantMessaging() bool
CanCreatePublicChannel() bool
CanCreatePrivateChannel() bool
CanCreateDirectChannel() bool
CanUpdate(ch *types.Channel) bool
CanRead(ch *types.Channel) bool
CanJoin(ch *types.Channel) bool
CanLeave(ch *types.Channel) bool
CanManageMembers(ch *types.Channel) bool
CanManageWebhooks(ch *types.Channel) bool
CanManageAttachments(ch *types.Channel) bool
CanSendMessage(ch *types.Channel) bool
CanReplyMessage(ch *types.Channel) bool
CanEmbedMessage(ch *types.Channel) bool
CanAttachMessage(ch *types.Channel) bool
CanUpdateOwnMessages(ch *types.Channel) bool
CanUpdateMessages(ch *types.Channel) bool
CanReactMessage(ch *types.Channel) bool
}
)
func Permissions() PermissionsService {
return (&permissions{
prm: systemService.Permissions(),
}).With(context.Background())
}
func (p *permissions) With(ctx context.Context) PermissionsService {
db := repository.DB(ctx)
return &permissions{
db: db,
ctx: ctx,
prm: p.prm.With(ctx),
}
}
func (p *permissions) CanAccessMessaging() bool {
return p.checkAccess("messaging", "access")
}
func (p *permissions) CanGrantMessaging() bool {
return p.checkAccess("messaging", "grant")
}
func (p *permissions) CanCreatePublicChannel() bool {
return p.checkAccess("messaging", "channel.public.create")
}
func (p *permissions) CanCreatePrivateChannel() bool {
return p.checkAccess("messaging", "channel.private.create")
}
func (p *permissions) CanCreateDirectChannel() bool {
return p.checkAccess("messaging", "channel.direct.create")
}
func (p *permissions) CanUpdate(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "update")
}
func (p *permissions) CanRead(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "read")
}
func (p *permissions) CanJoin(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "join")
}
func (p *permissions) CanLeave(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "leave")
}
func (p *permissions) CanManageMembers(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "members.manage")
}
func (p *permissions) CanManageWebhooks(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "webhooks.manage")
}
func (p *permissions) CanManageAttachments(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "attachments.manage")
}
func (p *permissions) CanSendMessage(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "message.send")
}
func (p *permissions) CanReplyMessage(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "message.reply")
}
func (p *permissions) CanEmbedMessage(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "message.embed")
}
func (p *permissions) CanAttachMessage(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "message.attach")
}
func (p *permissions) CanUpdateOwnMessages(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "message.update.own")
}
func (p *permissions) CanUpdateMessages(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "message.update.all")
}
func (p *permissions) CanReactMessage(ch *types.Channel) bool {
return p.checkAccess(ch.Resource().String(), "message.react")
}
func (p *permissions) checkAccess(resource string, operation string, fbs ...Fallback) bool {
access := p.prm.Check(resource, operation)
switch access {
case internalRules.Allow:
return true
case internalRules.Deny:
return false
default:
for _, fb := range fbs {
if fb() == true {
return true
}
}
return false
}
}

View File

@@ -0,0 +1,115 @@
package service
import (
"context"
"testing"
"github.com/crusttech/crust/internal/auth"
"github.com/crusttech/crust/internal/rules"
. "github.com/crusttech/crust/internal/test"
"github.com/crusttech/crust/messaging/types"
systemService "github.com/crusttech/crust/system/service"
systemTypes "github.com/crusttech/crust/system/types"
)
func TestPermissions(t *testing.T) {
ctx := context.TODO()
// Create user with role and add it to context.
userSvc := systemService.User().With(ctx)
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 = userSvc.Create(user)
NoError(t, err, "expected no error creating user, got %v", err)
roleSvc := systemService.Role().With(ctx)
role := &systemTypes.Role{
Name: "Test role v1",
}
role, err = roleSvc.Create(role)
NoError(t, err, "expected no error creating role, got %v", err)
err = roleSvc.MemberAdd(role.ID, user.ID)
NoError(t, err, "expected no error adding user to role, got %v", err)
// Set Identity.
ctx = auth.SetIdentityToContext(ctx, user)
// Generate services.
channelSvc := Channel().With(ctx)
permissionsSvc := Permissions().With(ctx)
systemPermissionSvc := systemService.Permissions().With(ctx)
// Test `access` to messaging service.
ret := permissionsSvc.CanAccessMessaging()
Assert(t, ret == false, "expected CanAccessMessaging == false, got %v", ret)
// Add `access` to messaging service.
list := []rules.Rule{
rules.Rule{Resource: "messaging", Operation: "access", Value: rules.Allow},
}
_, err = systemPermissionSvc.Update(role.ID, list)
NoError(t, err, "expected no error, got %v", err)
// Test `access` to messaging service.
ret = permissionsSvc.CanAccessMessaging()
Assert(t, ret == true, "expected CanAccessMessaging == true, got %v", ret)
// Create test channel.
ch := &types.Channel{
Name: "TestChan",
Topic: "No topic",
}
ch, err = channelSvc.Create(ch)
NoError(t, err, "expected no error, got %v", err)
// @Todo: add permission for create channel and test it.
// Test CanRead permissions. [1 - no permission, 2 - allow]
{
ret = permissionsSvc.CanRead(ch)
Assert(t, ret == false, "expected CanRead == false, got %v")
// Add [messaging:channel:*, read, allow]
list = []rules.Rule{
rules.Rule{Resource: "messaging:channel:*", Operation: "read", Value: rules.Allow},
}
_, err = systemPermissionSvc.Update(role.ID, list)
NoError(t, err, "expected no error, got %v", err)
ret = permissionsSvc.CanRead(ch)
Assert(t, ret == true, "expected CanRead == true, got %v")
}
// Test CanJoin permissions [1 - deny, 2 - allow for resourceID]
{
// Add [messaging:channel:*, join, deny]
list = []rules.Rule{
rules.Rule{Resource: "messaging:channel:*", Operation: "join", Value: rules.Deny},
}
_, err = systemPermissionSvc.Update(role.ID, list)
NoError(t, err, "expected no error, got %v", err)
ret = permissionsSvc.CanJoin(ch)
Assert(t, ret == false, "expected CanJoin == false, got %v")
// Add [messaging:channel:ID, join, allow]
list = []rules.Rule{
rules.Rule{Resource: ch.Resource().String(), Operation: "join", Value: rules.Allow},
}
_, err = systemPermissionSvc.Update(role.ID, list)
NoError(t, err, "expected no error, got %v", err)
ret = permissionsSvc.CanJoin(ch)
Assert(t, ret == true, "expected CanJoin == true, got %v")
}
// Remove test channel.
channelSvc.Delete(ch.ID)
}

View File

@@ -1,143 +0,0 @@
package service
import (
"context"
internalRules "github.com/crusttech/crust/internal/rules"
"github.com/crusttech/crust/messaging/repository"
"github.com/crusttech/crust/messaging/types"
systemTypes "github.com/crusttech/crust/system/types"
)
type (
rules struct {
db db
ctx context.Context
// identity is passed with context
resources internalRules.ResourcesInterface
role *systemTypes.Role
org *types.Organisation
}
RulesService interface {
With(context.Context) RulesService
// Applies mostly to admin panel
isAdmin() bool
// Individual rules for administration
canManageOrganisation() bool
canManageRoles() bool
canManageChannels() bool
canManageWebhooks(ch *types.Channel) bool
// Messaging rules
canSendMessages(ch *types.Channel) bool
canEmbedLinks(ch *types.Channel) bool
canAttachFiles(ch *types.Channel) bool
canUpdateOwnMessages(ch *types.Channel) bool
canUpdateMessages(ch *types.Channel) bool
canReact(ch *types.Channel) bool
}
)
func Rules() RulesService {
return (&rules{
role: &systemTypes.Role{},
}).With(context.Background())
}
func (r *rules) With(ctx context.Context) RulesService {
db := repository.DB(ctx)
org := repository.Organization(ctx)
return &rules{
db: db,
ctx: ctx,
org: org,
role: r.role,
resources: internalRules.NewResources(ctx, db),
}
}
// @todo: honor defaults from (org/team/channel).Permissions()
func (r *rules) isAdmin() bool {
op := "admin"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String())
}
func (r *rules) canManageOrganisation() bool {
op := "manage.organisation"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String())
}
func (r *rules) canManageRoles() bool {
op := "manage.roles"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String())
}
func (r *rules) canManageChannels() bool {
op := "manage.channels"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String())
}
func (r *rules) canManageWebhooks(ch *types.Channel) bool {
op := "manage.webhooks"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String(), r.role.Resource().All(), ch.Resource().String())
}
func (r *rules) canSendMessages(ch *types.Channel) bool {
op := "message.send"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String(), r.role.Resource().All(), ch.Resource().String())
}
func (r *rules) canEmbedLinks(ch *types.Channel) bool {
op := "message.embed"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String(), r.role.Resource().All(), ch.Resource().String())
}
func (r *rules) canAttachFiles(ch *types.Channel) bool {
op := "message.attach"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String(), r.role.Resource().All(), ch.Resource().String())
}
func (r *rules) canUpdateOwnMessages(ch *types.Channel) bool {
op := "message.update_own"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String(), r.role.Resource().All(), ch.Resource().String())
}
func (r *rules) canUpdateMessages(ch *types.Channel) bool {
op := "message.update_all"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String(), r.role.Resource().All(), ch.Resource().String())
}
func (r *rules) canReact(ch *types.Channel) bool {
op := "message.react"
return r.hasAccess(op, r.org.PermissionDefault(op), r.org.Resource().String(), r.role.Resource().All(), ch.Resource().String())
}
func (r *rules) hasAccess(operation string, value internalRules.Access, scopes ...string) bool {
// reverse scopes from to order it from most-least significant
// aka: [0]channel [1]teams [2]org
last := len(scopes) - 1
for i := 0; i < len(scopes)/2; i++ {
scopes[i], scopes[last-i] = scopes[last-i], scopes[i]
}
for _, scope := range scopes {
if scope == "" {
continue
}
switch r.resources.IsAllowed(scope, operation) {
case internalRules.Allow:
return true
case internalRules.Deny:
return false
default: // inherit
}
}
return false
}

View File

@@ -5,10 +5,7 @@ import (
"sync"
"time"
internalRules "github.com/crusttech/crust/internal/rules"
"github.com/crusttech/crust/internal/store"
"github.com/crusttech/crust/messaging/types"
)
type (
@@ -18,12 +15,13 @@ type (
)
var (
o sync.Once
DefaultAttachment AttachmentService
DefaultChannel ChannelService
DefaultMessage MessageService
DefaultPubSub *pubSub
DefaultEvent EventService
o sync.Once
DefaultAttachment AttachmentService
DefaultChannel ChannelService
DefaultMessage MessageService
DefaultPubSub *pubSub
DefaultEvent EventService
DefaultPermissions PermissionsService
)
func Init() {
@@ -33,11 +31,7 @@ func Init() {
log.Fatalf("Failed to initialize stor: %v", err)
}
scopes := internalRules.NewScope()
scopes.Add(&types.Organisation{})
scopes.Add(&types.Role{})
scopes.Add(&types.Channel{})
DefaultPermissions = Permissions()
DefaultEvent = Event()
DefaultAttachment = Attachment(fs)
DefaultMessage = Message()

View File

@@ -58,7 +58,8 @@ type (
// Resource returns a system resource ID for this type
func (r *Channel) Resource() rules.Resource {
resource := rules.Resource{
Scope: "channel",
Service: "messaging",
Scope: "channel",
}
if r != nil {
resource.ID = r.ID

View File

@@ -21,7 +21,7 @@ type (
func (Permissions) New() *Permissions {
ctrl := &Permissions{}
ctrl.svc.perm = service.DefaultPermission
ctrl.svc.perm = service.DefaultPermissions
return ctrl
}

View File

@@ -20,13 +20,16 @@ type (
With(ctx context.Context) PermissionsService
List() (interface{}, error)
Check(resource string, operation string) rules.Access
Read(roleID uint64) (interface{}, error)
Update(roleID uint64, rules []rules.Rule) (interface{}, error)
Delete(roleID uint64) (interface{}, error)
}
)
func Permission() PermissionsService {
func Permissions() PermissionsService {
return (&permissions{}).With(context.Background())
}
@@ -50,6 +53,10 @@ func (p *permissions) List() (interface{}, error) {
return perms, nil
}
func (p *permissions) Check(resource string, operation string) rules.Access {
return p.resources.Check(resource, operation)
}
func (p *permissions) Read(roleID uint64) (interface{}, error) {
return p.resources.Read(roleID)
}

View File

@@ -48,7 +48,7 @@ func TestPermission(t *testing.T) {
ctx = internalAuth.SetIdentityToContext(ctx, user)
// Create permission service.
permissionSvc := Permission().With(ctx)
permissionSvc := Permissions().With(ctx)
// Update rules for test role.
{

View File

@@ -15,7 +15,7 @@ var (
DefaultAuth AuthService
DefaultUser UserService
DefaultRole RoleService
DefaultPermission PermissionsService
DefaultPermissions PermissionsService
DefaultOrganisation OrganisationService
)
@@ -24,7 +24,7 @@ func Init() {
DefaultAuth = Auth()
DefaultUser = User()
DefaultRole = Role()
DefaultPermission = Permission()
DefaultPermissions = Permissions()
DefaultOrganisation = Organisation()
})
}

View File

@@ -26,7 +26,8 @@ type (
// Resource returns a system resource ID for this type
func (r *Organisation) Resource() rules.Resource {
resource := rules.Resource{
Scope: "organisation",
Service: "system",
Scope: "organisation",
}
if r != nil {
resource.ID = r.ID

View File

@@ -26,7 +26,8 @@ type (
// Resource returns a system resource ID for this type
func (r *Role) Resource() rules.Resource {
resource := rules.Resource{
Scope: "team",
Service: "system",
Scope: "role",
}
if r != nil {
resource.ID = r.ID