Merge branch 'user-data-obf'
This commit is contained in:
@@ -43,6 +43,10 @@ func (ac AccessCheck) HasOperation() bool {
|
||||
return ac.operation != ""
|
||||
}
|
||||
|
||||
func (ac AccessCheck) HasResource() bool {
|
||||
return ac.resource != ""
|
||||
}
|
||||
|
||||
// ToSql converts access check to SQL (with args) that will help with filtering
|
||||
//
|
||||
// Satisfies squirrel.Sqlizer interface
|
||||
|
||||
51
pkg/rh/access_check.go
Normal file
51
pkg/rh/access_check.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package rh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Masterminds/squirrel.v1"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/permissions"
|
||||
)
|
||||
|
||||
type (
|
||||
expAccessCheck struct {
|
||||
ac permissions.AccessCheck
|
||||
expr squirrel.Sqlizer
|
||||
}
|
||||
)
|
||||
|
||||
// ExpAccessCheck wraps expression into access control check
|
||||
//
|
||||
// If access-check is enabled and user can not perform an operation
|
||||
// condition will fail
|
||||
func ExpAccessCheck(ac permissions.AccessCheck, expr squirrel.Sqlizer) expAccessCheck {
|
||||
if ac.HasOperation() && !ac.HasResource() {
|
||||
// This is not runtime error
|
||||
// developer should make sure access control has resource!
|
||||
panic("can not wrap expression with access check without resource")
|
||||
}
|
||||
|
||||
return expAccessCheck{
|
||||
ac: ac,
|
||||
expr: expr,
|
||||
}
|
||||
}
|
||||
|
||||
func (eac expAccessCheck) ToSql() (string, []interface{}, error) {
|
||||
if !eac.ac.HasOperation() {
|
||||
return eac.expr.ToSql()
|
||||
}
|
||||
|
||||
acSql, acArgs, err := eac.ac.ToSql()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
sql, args, err := eac.expr.ToSql()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`IF(%s, %s, false)`, acSql, sql), append(acArgs, args...), nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/titpetric/factory"
|
||||
"gopkg.in/Masterminds/squirrel.v1"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
)
|
||||
|
||||
@@ -127,37 +128,46 @@ func (r user) Find(filter types.UserFilter) (set types.UserSet, f types.UserFilt
|
||||
f = filter
|
||||
q := r.queryNoFilter()
|
||||
|
||||
f.AccessCheckEmail.BindToEnv(types.UserPermissionResource, "sys")
|
||||
f.AccessCheckName.BindToEnv(types.UserPermissionResource, "sys")
|
||||
|
||||
if !f.IncDeleted {
|
||||
q = q.Where("u.deleted_at IS NULL")
|
||||
q = q.Where(squirrel.Eq{"u.deleted_at": nil})
|
||||
}
|
||||
|
||||
if !f.IncSuspended {
|
||||
q = q.Where("u.suspended_at IS NULL")
|
||||
q = q.Where(squirrel.Eq{"u.suspended_at": nil})
|
||||
}
|
||||
|
||||
if f.Query != "" {
|
||||
qs := f.Query + "%"
|
||||
q = q.Where("u.username LIKE ? OR u.email LIKE ? OR u.name LIKE ?", qs, qs, qs)
|
||||
q = q.Where(squirrel.Or{
|
||||
squirrel.Like{"u.username": qs},
|
||||
squirrel.Like{"u.handle": qs},
|
||||
rh.ExpAccessCheck(f.AccessCheckEmail, squirrel.Like{"u.email": qs}),
|
||||
rh.ExpAccessCheck(f.AccessCheckName, squirrel.Like{"u.name": qs}),
|
||||
})
|
||||
}
|
||||
|
||||
if f.Email != "" {
|
||||
q = q.Where("u.email = ?", f.Email)
|
||||
q = q.Where(rh.ExpAccessCheck(f.AccessCheckEmail, squirrel.Eq{"u.email": f.Email}))
|
||||
}
|
||||
|
||||
if f.Username != "" {
|
||||
q = q.Where("u.username = ?", f.Username)
|
||||
q = q.Where(squirrel.Eq{"u.username": f.Username})
|
||||
}
|
||||
|
||||
if f.Handle != "" {
|
||||
q = q.Where("u.handle = ?", f.Handle)
|
||||
q = q.Where(squirrel.Eq{"u.handle": f.Handle})
|
||||
}
|
||||
|
||||
if f.Kind != "" {
|
||||
q = q.Where("u.kind = ?", f.Kind)
|
||||
q = q.Where(squirrel.Eq{"u.kind": f.Kind})
|
||||
}
|
||||
|
||||
if f.Email != "" {
|
||||
q = q.Where("u.email = ?", f.Email)
|
||||
if f.AccessCheck.HasOperation() {
|
||||
// Filter users based on what current user can read
|
||||
q = q.Where(f.AccessCheck.BindToEnv(types.UserPermissionResource, "sys"))
|
||||
}
|
||||
|
||||
// @todo add support for more sophisticated sorting through ql
|
||||
@@ -180,7 +190,13 @@ func (r user) Find(filter types.UserFilter) (set types.UserSet, f types.UserFilt
|
||||
q = q.OrderBy("id")
|
||||
}
|
||||
|
||||
return set, f, r.fetchPaged(&set, q, f.Page, f.PerPage)
|
||||
db := r.db()
|
||||
|
||||
if f.Count, err = rh.Count(db, q); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return set, f, rh.FetchPaged(db, q, f.Page, f.PerPage, &set)
|
||||
}
|
||||
|
||||
func (r user) Total() (count uint) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/payload"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
"github.com/cortezaproject/corteza-server/system/rest/request"
|
||||
"github.com/cortezaproject/corteza-server/system/service"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
@@ -42,8 +43,8 @@ func (ctrl User) List(ctx context.Context, r *request.UserList) (interface{}, er
|
||||
Kind: r.Kind,
|
||||
IncSuspended: r.IncSuspended,
|
||||
IncDeleted: r.IncDeleted,
|
||||
Page: r.Page,
|
||||
PerPage: r.PerPage,
|
||||
|
||||
PageFilter: rh.Paging(r.Page, r.PerPage),
|
||||
}
|
||||
|
||||
set, filter, err := ctrl.user.With(ctx).Find(f)
|
||||
|
||||
@@ -113,6 +113,18 @@ func (svc accessControl) CanDeleteApplication(ctx context.Context, app *types.Ap
|
||||
return svc.can(ctx, app, "delete")
|
||||
}
|
||||
|
||||
func (svc accessControl) CanReadAnyUser(ctx context.Context) bool {
|
||||
return svc.can(ctx, types.UserPermissionResource.AppendWildcard(), "read")
|
||||
}
|
||||
|
||||
func (svc accessControl) CanUnmaskEmailOnAnyUser(ctx context.Context) bool {
|
||||
return svc.can(ctx, types.UserPermissionResource.AppendWildcard(), "unmask.email")
|
||||
}
|
||||
|
||||
func (svc accessControl) CanUnmaskNameOnAnyUser(ctx context.Context) bool {
|
||||
return svc.can(ctx, types.UserPermissionResource.AppendWildcard(), "unmask.name")
|
||||
}
|
||||
|
||||
func (svc accessControl) CanUpdateUser(ctx context.Context, u *types.User) bool {
|
||||
return svc.can(ctx, u, "update")
|
||||
}
|
||||
@@ -129,6 +141,14 @@ func (svc accessControl) CanDeleteUser(ctx context.Context, u *types.User) bool
|
||||
return svc.can(ctx, u, "delete")
|
||||
}
|
||||
|
||||
func (svc accessControl) CanUnmaskEmail(ctx context.Context, u *types.User) bool {
|
||||
return svc.can(ctx, u, "unmask.email")
|
||||
}
|
||||
|
||||
func (svc accessControl) CanUnmaskName(ctx context.Context, u *types.User) bool {
|
||||
return svc.can(ctx, u, "unmask.name")
|
||||
}
|
||||
|
||||
func (svc accessControl) CanReadAnyAutomationScript(ctx context.Context) bool {
|
||||
return svc.can(ctx, types.AutomationScriptPermissionResource.AppendWildcard(), "read")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
internalAuth "github.com/cortezaproject/corteza-server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza-server/pkg/permissions"
|
||||
"github.com/cortezaproject/corteza-server/system/repository"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
)
|
||||
@@ -21,6 +22,9 @@ const (
|
||||
ErrUserUsernameNotUnique = serviceError("UserUsernameNotUnique")
|
||||
ErrUserEmailNotUnique = serviceError("UserEmailNotUnique")
|
||||
ErrUserLocked = serviceError("UserLocked")
|
||||
|
||||
maskPrivateDataEmail = "####.#######@######.###"
|
||||
maskPrivateDataName = "##### ##########"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -36,6 +40,11 @@ type (
|
||||
ac userAccessController
|
||||
user repository.UserRepository
|
||||
credentials repository.CredentialsRepository
|
||||
|
||||
// @todo wire this with settings (privacy.mask.email)
|
||||
privacyMaskEmail bool
|
||||
// @todo wire this with settings (privacy.mask.name)
|
||||
privacyMaskName bool
|
||||
}
|
||||
|
||||
userAuth interface {
|
||||
@@ -45,11 +54,16 @@ type (
|
||||
|
||||
userAccessController interface {
|
||||
CanAccess(context.Context) bool
|
||||
CanReadAnyUser(context.Context) bool
|
||||
CanUnmaskEmailOnAnyUser(context.Context) bool
|
||||
CanUnmaskNameOnAnyUser(context.Context) bool
|
||||
CanCreateUser(context.Context) bool
|
||||
CanUpdateUser(context.Context, *types.User) bool
|
||||
CanDeleteUser(context.Context, *types.User) bool
|
||||
CanSuspendUser(context.Context, *types.User) bool
|
||||
CanUnsuspendUser(context.Context, *types.User) bool
|
||||
CanUnmaskEmail(context.Context, *types.User) bool
|
||||
CanUnmaskName(context.Context, *types.User) bool
|
||||
}
|
||||
|
||||
UserService interface {
|
||||
@@ -101,6 +115,14 @@ func (svc user) With(ctx context.Context) UserService {
|
||||
|
||||
user: repository.User(ctx, db),
|
||||
credentials: repository.Credentials(ctx, db),
|
||||
|
||||
// @todo wire this with settings (privacy.mask.email)
|
||||
// new default value will be true!
|
||||
privacyMaskEmail: false,
|
||||
|
||||
// @todo wire this with settings (privacy.mask.name)
|
||||
// new default value will be true!
|
||||
privacyMaskName: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,29 +151,64 @@ func (svc user) proc(u *types.User, err error) (*types.User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc.handlePrivateData(u)
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (svc user) FindByIDs(userIDs ...uint64) (types.UserSet, error) {
|
||||
return svc.procSet(svc.user.FindByIDs(userIDs...))
|
||||
uu, err := svc.user.FindByIDs(userIDs...)
|
||||
uu, _, err = svc.procSet(uu, types.UserFilter{}, err)
|
||||
return uu, err
|
||||
}
|
||||
|
||||
func (svc user) Find(f types.UserFilter) (types.UserSet, types.UserFilter, error) {
|
||||
if f.IncDeleted || f.IncSuspended {
|
||||
// If list with deleted or suspended users is requested
|
||||
// user must have access permissions to system (ie: is admin)
|
||||
if !svc.ac.CanAccess(svc.ctx) {
|
||||
return nil, f, ErrNoPermissions.withStack()
|
||||
}
|
||||
}
|
||||
|
||||
return svc.user.Find(f)
|
||||
}
|
||||
|
||||
func (svc user) procSet(u types.UserSet, err error) (types.UserSet, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if svc.privacyMaskEmail {
|
||||
// Prepare filter for email unmasking check
|
||||
f.AccessCheckEmail = permissions.InitAccessCheckFilter(
|
||||
"unmask.email",
|
||||
internalAuth.GetIdentityFromContext(svc.ctx).Roles(),
|
||||
svc.ac.CanUnmaskEmailOnAnyUser(svc.ctx),
|
||||
)
|
||||
}
|
||||
|
||||
return u, nil
|
||||
if svc.privacyMaskName {
|
||||
// Prepare filter for name unmasking check
|
||||
f.AccessCheckName = permissions.InitAccessCheckFilter(
|
||||
"unmask.name",
|
||||
internalAuth.GetIdentityFromContext(svc.ctx).Roles(),
|
||||
svc.ac.CanUnmaskNameOnAnyUser(svc.ctx),
|
||||
)
|
||||
}
|
||||
|
||||
f.AccessCheck = permissions.InitAccessCheckFilter(
|
||||
"read",
|
||||
internalAuth.GetIdentityFromContext(svc.ctx).Roles(),
|
||||
svc.ac.CanReadAnyUser(svc.ctx),
|
||||
)
|
||||
|
||||
return svc.procSet(svc.user.Find(f))
|
||||
}
|
||||
|
||||
func (svc user) procSet(u types.UserSet, f types.UserFilter, err error) (types.UserSet, types.UserFilter, error) {
|
||||
if err != nil {
|
||||
return nil, f, err
|
||||
}
|
||||
|
||||
_ = u.Walk(func(u *types.User) error {
|
||||
svc.handlePrivateData(u)
|
||||
return nil
|
||||
})
|
||||
|
||||
return u, f, nil
|
||||
}
|
||||
|
||||
func (svc user) Create(input *types.User) (out *types.User, err error) {
|
||||
@@ -323,3 +380,14 @@ func (svc user) SetPassword(userID uint64, newPassword string) (err error) {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Masks (or leaves as-is) private data on user
|
||||
func (svc user) handlePrivateData(u *types.User) {
|
||||
if svc.privacyMaskEmail && !svc.ac.CanUnmaskEmail(svc.ctx, u) {
|
||||
u.Email = maskPrivateDataEmail
|
||||
}
|
||||
|
||||
if svc.privacyMaskName && !svc.ac.CanUnmaskEmail(svc.ctx, u) {
|
||||
u.Name = maskPrivateDataName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/permissions"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -51,10 +52,19 @@ type (
|
||||
IncDeleted bool `json:"incDeleted"`
|
||||
IncSuspended bool `json:"incSuspended"`
|
||||
|
||||
Page uint `json:"page"`
|
||||
PerPage uint `json:"perPage"`
|
||||
Sort string `json:"sort"`
|
||||
Count uint `json:"count"`
|
||||
Sort string `json:"sort"`
|
||||
|
||||
// Can we use email for searching or is it supposed to be masked for the current user?
|
||||
AccessCheckEmail permissions.AccessCheck `json:"-"`
|
||||
|
||||
// Can we use name for searching or is it supposed to be masked for the current user?
|
||||
AccessCheckName permissions.AccessCheck `json:"-"`
|
||||
|
||||
// Access check for user
|
||||
permissions.AccessCheck `json:"-"`
|
||||
|
||||
// Standard paging fields & helpers
|
||||
rh.PageFilter
|
||||
}
|
||||
|
||||
UserKind string
|
||||
|
||||
Reference in New Issue
Block a user