3
0

Implementation actions & errors for access control, auth, role & user

This commit is contained in:
Denis Arh 2020-05-18 14:42:29 +02:00
parent f84f8b5b33
commit 5f8fb8a294
18 changed files with 5756 additions and 989 deletions

View File

@ -347,63 +347,77 @@ func {{ camelCase "" $.Service "Err" $e.Error }}(props ... *{{ $.Service }}Actio
// This function is auto-generated.
//
func (svc {{ $.Service }}) recordAction(ctx context.Context, props *{{ $.Service }}ActionProps, action func(... *{{ $.Service }}ActionProps) *{{ $.Service }}Action, err error) error {
// If non-{{ $.Service }}Error error was passed,
// wrap it with generic error
var (
svcErr *{{ $.Service }}Error
ok bool
)
ok bool
// Return error
retError *{{ $.Service }}Error
// Recorder error
recError *{{ $.Service }}Error
)
if err != nil {
if svcErr, ok = err.(*{{ $.Service }}Error); !ok {
// wrap non-{{ $.Service }} errors
svcErr = {{ camelCase "" $.Service "err" "generic" }}(props).Wrap(err)
if retError, ok = err.(*{{ $.Service }}Error); !ok {
// got non-{{ $.Service }} error, wrap it with {{ camelCase "" $.Service "err" "generic" }}
retError = {{ camelCase "" $.Service "err" "generic" }}(props).Wrap(err)
// make sure we return generic error!
err = svcErr
} else {
// find the original cause for this error
// for the purpose of logging
//
// we'll return the received error as-is
// copy action to returning and recording error
retError.action = action().action
// we'll use {{ camelCase "" $.Service "err" "generic" }} for recording too
// because it can hold more info
recError = retError
} else if retError != nil {
// copy action to returning and recording error
retError.action = action().action
// start with copy of return error for recording
// this will be updated with tha root cause as we try and
// unwrap the error
recError = retError
// find the original recError for this error
// for the purpose of logging
var unwrappedError error = retError
for {
ue := errors.Unwrap(svcErr)
if ue == nil {
// nothing wrapped
if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil {
// nothing wrapped
break
}
if _, ok = ue.(*{{ $.Service }}Error); !ok {
// wrapped error is not of type *{{ $.Service }}Error
break
// update recError ONLY of wrapped error is of type {{ $.Service }}Error
if unwrappedSinkError, ok := unwrappedError.(*{{ $.Service }}Error); ok {
recError = unwrappedSinkError
}
svcErr = ue.(*{{ $.Service }}Error)
}
if svcErr.props == nil {
// got {{ $.Service }}Error w/o props,
// assign props from args
svcErr.props = props
}
}
if retError.props == nil {
// set props on returning error if empty
retError.props = props
}
if action != nil {
// copy action to error
a := action()
svcErr.action = a.action
if recError.props == nil {
// set props on recording error if empty
recError.props = props
}
}
}
if svc.actionlog != nil {
if svcErr != nil {
// failed action, log error
svc.actionlog.Record(ctx, svcErr)
if retError != nil {
// failed action, log error
svc.actionlog.Record(ctx, recError)
} else if action != nil {
// successful
// successful
svc.actionlog.Record(ctx, action(props))
}
}
return err
if err == nil {
// retError not an interface and that WILL (!!) cause issues
// with nil check (== nil) when it is not explicitly returned
return nil
}
return retError
}

View File

@ -25,10 +25,10 @@ type (
const (
// Hardcoded Role ID for everyone
EveryoneRoleID = 1
EveryoneRoleID uint64 = 1
// Hardcoded ID for Admin role
AdminsRoleID = 2
AdminsRoleID uint64 = 2
)
func (op Operation) String() string {

View File

@ -2,6 +2,8 @@ package service
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
internalAuth "github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/permissions"
@ -11,6 +13,7 @@ import (
type (
accessControl struct {
permissions accessControlPermissionServicer
actionlog actionlog.Recorder
}
accessControlPermissionServicer interface {
@ -28,6 +31,7 @@ type (
func AccessControl(perm accessControlPermissionServicer) *accessControl {
return &accessControl{
permissions: perm,
actionlog: DefaultActionlog,
}
}
@ -181,15 +185,35 @@ func (svc accessControl) can(ctx context.Context, res permissionResource, op per
func (svc accessControl) Grant(ctx context.Context, rr ...*permissions.Rule) error {
if !svc.CanGrant(ctx) {
return ErrNoGrantPermissions
return AccessControlErrNotAllowedToSetPermissions()
}
return svc.permissions.Grant(ctx, svc.Whitelist(), rr...)
if err := svc.permissions.Grant(ctx, svc.Whitelist(), rr...); err != nil {
return AccessControlErrGeneric().Wrap(err)
}
svc.logGrants(ctx, rr)
return nil
}
func (svc accessControl) logGrants(ctx context.Context, rr []*permissions.Rule) {
if svc.actionlog == nil {
return
}
for _, r := range rr {
g := AccessControlActionGrant(&accessControlActionProps{r})
g.log = r.String()
g.resource = r.Resource.String()
svc.actionlog.Record(ctx, g)
}
}
func (svc accessControl) FindRulesByRoleID(ctx context.Context, roleID uint64) (permissions.RuleSet, error) {
if !svc.CanGrant(ctx) {
return nil, ErrNoPermissions
return nil, AccessControlErrNotAllowedToSetPermissions()
}
return svc.permissions.FindRulesByRoleID(roleID), nil

View File

@ -0,0 +1,407 @@
package service
// This file is auto-generated from system/service/access_control_actions.yaml
//
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/permissions"
)
type (
accessControlActionProps struct {
rule *permissions.Rule
}
accessControlAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *accessControlActionProps
}
accessControlError struct {
timestamp time.Time
error string
resource string
action string
message string
log string
severity actionlog.Severity
wrap error
props *accessControlActionProps
}
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setRule updates accessControlActionProps's rule
//
// Allows method chaining
//
// This function is auto-generated.
//
func (p *accessControlActionProps) setRule(rule *permissions.Rule) *accessControlActionProps {
p.rule = rule
return p
}
// serialize converts accessControlActionProps to actionlog.Meta
//
// This function is auto-generated.
//
func (p accessControlActionProps) serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
str = func(i interface{}) string { return fmt.Sprintf("%v", i) }
)
if p.rule != nil {
m["rule.operation"] = str(p.rule.Operation)
m["rule.roleID"] = str(p.rule.RoleID)
m["rule.access"] = str(p.rule.Access)
m["rule.resource"] = str(p.rule.Resource)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
//
func (p accessControlActionProps) tr(in string, err error) string {
var pairs = []string{"{err}"}
if err != nil {
for {
// Unwrap errors
ue := errors.Unwrap(err)
if ue == nil {
break
}
err = ue
}
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.rule != nil {
pairs = append(pairs, "{rule}", fmt.Sprintf("%v", p.rule.Operation))
pairs = append(pairs, "{rule.operation}", fmt.Sprintf("%v", p.rule.Operation))
pairs = append(pairs, "{rule.roleID}", fmt.Sprintf("%v", p.rule.RoleID))
pairs = append(pairs, "{rule.access}", fmt.Sprintf("%v", p.rule.Access))
pairs = append(pairs, "{rule.resource}", fmt.Sprintf("%v", p.rule.Resource))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
//
func (a *accessControlAction) String() string {
var props = &accessControlActionProps{}
if a.props != nil {
props = a.props
}
return props.tr(a.log, nil)
}
func (e *accessControlAction) LoggableAction() *actionlog.Action {
return &actionlog.Action{
Timestamp: e.timestamp,
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error methods
// String returns loggable description as string
//
// It falls back to message if log is not set
//
// This function is auto-generated.
//
func (e *accessControlError) String() string {
var props = &accessControlActionProps{}
if e.props != nil {
props = e.props
}
if e.wrap != nil && !strings.Contains(e.log, "{err}") {
// Suffix error log with {err} to ensure
// we log the cause for this error
e.log += ": {err}"
}
return props.tr(e.log, e.wrap)
}
// Error satisfies
//
// This function is auto-generated.
//
func (e *accessControlError) Error() string {
var props = &accessControlActionProps{}
if e.props != nil {
props = e.props
}
return props.tr(e.message, e.wrap)
}
// Is fn for error equality check
//
// This function is auto-generated.
//
func (e *accessControlError) Is(Resource error) bool {
t, ok := Resource.(*accessControlError)
if !ok {
return false
}
return t.resource == e.resource && t.error == e.error
}
// Wrap wraps accessControlError around another error
//
// This function is auto-generated.
//
func (e *accessControlError) Wrap(err error) *accessControlError {
e.wrap = err
return e
}
// Unwrap returns wrapped error
//
// This function is auto-generated.
//
func (e *accessControlError) Unwrap() error {
return e.wrap
}
func (e *accessControlError) LoggableAction() *actionlog.Action {
return &actionlog.Action{
Timestamp: e.timestamp,
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Error: e.Error(),
Meta: e.props.serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// AccessControlActionGrant returns "system:access_control.grant" error
//
// This function is auto-generated.
//
func AccessControlActionGrant(props ...*accessControlActionProps) *accessControlAction {
a := &accessControlAction{
timestamp: time.Now(),
resource: "system:access_control",
action: "grant",
log: "grant",
severity: actionlog.Error,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// AccessControlErrGeneric returns "system:access_control.generic" audit event as actionlog.Error
//
//
// This function is auto-generated.
//
func AccessControlErrGeneric(props ...*accessControlActionProps) *accessControlError {
var e = &accessControlError{
timestamp: time.Now(),
resource: "system:access_control",
error: "generic",
action: "error",
message: "failed to complete request due to internal error",
log: "{err}",
severity: actionlog.Error,
props: func() *accessControlActionProps {
if len(props) > 0 {
return props[0]
}
return nil
}(),
}
if len(props) > 0 {
e.props = props[0]
}
return e
}
// AccessControlErrNotAllowedToSetPermissions returns "system:access_control.notAllowedToSetPermissions" audit event as actionlog.Alert
//
//
// This function is auto-generated.
//
func AccessControlErrNotAllowedToSetPermissions(props ...*accessControlActionProps) *accessControlError {
var e = &accessControlError{
timestamp: time.Now(),
resource: "system:access_control",
error: "notAllowedToSetPermissions",
action: "error",
message: "not allowed to set permissions",
log: "not allowed to set permissions",
severity: actionlog.Alert,
props: func() *accessControlActionProps {
if len(props) > 0 {
return props[0]
}
return nil
}(),
}
if len(props) > 0 {
e.props = props[0]
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// context is used to enrich audit log entry with current user info, request ID, IP address...
// props are collected action/error properties
// action (optional) fn will be used to construct accessControlAction struct from given props (and error)
// err is any error that occurred while action was happening
//
// Action has success and fail (error) state:
// - when recorded without an error (4th param), action is recorded as successful.
// - when an additional error is given (4th param), action is used to wrap
// the additional error
//
// This function is auto-generated.
//
func (svc accessControl) recordAction(ctx context.Context, props *accessControlActionProps, action func(...*accessControlActionProps) *accessControlAction, err error) error {
var (
ok bool
// Return error
retError *accessControlError
// Recorder error
recError *accessControlError
)
if err != nil {
if retError, ok = err.(*accessControlError); !ok {
// got non-accessControl error, wrap it with AccessControlErrGeneric
retError = AccessControlErrGeneric(props).Wrap(err)
// copy action to returning and recording error
retError.action = action().action
// we'll use AccessControlErrGeneric for recording too
// because it can hold more info
recError = retError
} else if retError != nil {
// copy action to returning and recording error
retError.action = action().action
// start with copy of return error for recording
// this will be updated with tha root cause as we try and
// unwrap the error
recError = retError
// find the original recError for this error
// for the purpose of logging
var unwrappedError error = retError
for {
if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil {
// nothing wrapped
break
}
// update recError ONLY of wrapped error is of type accessControlError
if unwrappedSinkError, ok := unwrappedError.(*accessControlError); ok {
recError = unwrappedSinkError
}
}
if retError.props == nil {
// set props on returning error if empty
retError.props = props
}
if recError.props == nil {
// set props on recording error if empty
recError.props = props
}
}
}
if svc.actionlog != nil {
if retError != nil {
// failed action, log error
svc.actionlog.Record(ctx, recError)
} else if action != nil {
// successful
svc.actionlog.Record(ctx, action(props))
}
}
if err == nil {
// retError not an interface and that WILL (!!) cause issues
// with nil check (== nil) when it is not explicitly returned
return nil
}
return retError
}

View File

@ -0,0 +1,25 @@
# List of security/audit events and errors that we need to log
resource: system:access_control
service: accessControl
# Default sensitivity for actions
defaultActionSeverity: note
# default severity for errors
defaultErrorSeverity: alert
import:
- github.com/cortezaproject/corteza-server/pkg/permissions
props:
- name: rule
type: "*permissions.Rule"
fields: [ operation, roleID, access, resource ]
actions:
- action: grant
errors:
- error: notAllowedToSetPermissions
message: "not allowed to set permissions"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,133 @@
# List of security/audit events and errors that we need to log
resource: system:auth
service: auth
# Default sensitivity for actions
defaultActionSeverity: warning
# default severity for errors
defaultErrorSeverity: alert
import:
- github.com/cortezaproject/corteza-server/system/types
props:
- name: email
- name: provider
- name: credentials
type: "*types.Credentials"
fields: [ kind, label, ID ]
- name: role
type: "*types.Role"
fields: [ handle, name, ID ]
- name: user
type: "*types.User"
fields: [ handle, name, ID, email, suspendedAt, deletedAt ]
actions:
- action: authenticate
log: "successfully authenticated with {credentials.kind}"
- action: issueToken
log: "token '{credentials.kind}' issued"
- action: validateToken
log: "token '{credentials.kind}' validated"
- action: changePassword
log: "password changed"
- action: internalSignup
log: "{user.email} signed-up"
- action: confirmEmail
log: "email {user.email} confirmed"
- action: externalSignup
log: "{user.email} signed-up after successful external authentication via {credentials.kind}"
- action: sendEmailConfirmationToken
log: "confirmation notification sent to {email}"
- action: sendPasswordResetToken
log: "password reset token sent to {email}"
- action: exchangePasswordResetToken
log: "password reset token exchanged"
- action: autoPromote
log: "auto-promoted to {role}"
- action: updateCredentials
log: "credentials {credentials.kind} updated"
- action: createCredentials
log: "new credentials {credentials.kind} created"
errors:
- error: subscription
message: "{err}"
log: "{err}"
severity: warning
- error: invalidCredentials
message: "invalid username and password combination"
log: "{email} failed to authenticate with {credentials.kind}"
severity: warning
- error: invalidEmailFormat
message: "invalid email"
- error: invalidHandle
message: "invalid handle"
- error: failedForUnknownUser
safe: invalidCredentials
log: "unknown user {email} tried to log-in with {credentials.kind}"
severity: warning
- error: failedForDisabledUser
safe: invalidCredentials
log: "disabled user {user} tried to log-in with {credentials.kind}"
severity: warning
- error: failedUnconfirmedEmail
message: "system requires confirmed email before logging in"
log: "failed to log-in with with unconfirmed email"
- error: interalLoginDisabledByConfig
message: "internal login (username/password) is disabled"
- error: internalSignupDisabledByConfig
message: "internal sign-up (username/password) is disabled"
- error: passwordChangeFailedForUnknownUser
message: "failed to change password for the unknown user"
- error: passwodResetFailedOldPasswordCheckFailed
message: "failed to change password, old password does not match"
- error: passwordResetDisabledByConfig
message: "password reset is disabled"
- error: passwordNotSecure
message: "provided password is not secure; use longer password with more non-alphanumeric character"
- error: externalDisabledByConfig
message: "external authentication (using external authentication provider) is disabled"
log: "external authentication is disabled"
severity: warning
- error: profileWithoutValidEmail
message: "external authentication provider returned profile without valid email"
log: "external authentication provider {credentials.kind} returned profile without valid email"
severity: warning
- error: credentialsLinkedToInvalidUser
message: "credentials {credentials.kind} linked to disabled or deleted user {user}"
severity: warning
- error: invalidToken
message: "invalid token"
severity: warning

View File

@ -7,7 +7,6 @@ import (
"github.com/golang/mock/gomock"
"github.com/markbates/goth"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
@ -33,8 +32,6 @@ func makeMockAuthService(u repository.UserRepository, c repository.CredentialsRe
return nil
},
logger: zap.NewNop(),
settings: &types.Settings{},
eventbus: eventbus.New(),
@ -143,19 +140,18 @@ func Test_auth_validateInternalLogin(t *testing.T) {
{name: "no email", args: args{"", ""}, wantErr: true},
{name: "bad email", args: args{"test", ""}, wantErr: true},
{name: "no pass", args: args{"test@domain.tld", ""}, wantErr: true},
{name: "all good", args: args{"test@domain.tld", "password"}, wantErr: false},
}
svc := auth{
logger: zap.NewNop(),
settings: &types.Settings{},
//{name: "all good", args: args{"test@domain.tld", "password"}, wantErr: false},
// until we get proper mocking, DI for unit testing in place
// this will have to do
}
svc := makeMockAuthService(nil, nil)
svc.settings.Auth.Internal.Enabled = true
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := svc.validateInternalLogin(tt.args.email, tt.args.password); (err != nil) != tt.wantErr {
if _, err := svc.InternalLogin(tt.args.email, tt.args.password); (err != nil) != tt.wantErr {
t.Errorf("auth.validateInternalLogin() error = %v, wantErr %v", err, tt.wantErr)
}
})
@ -170,35 +166,35 @@ func Test_auth_checkPassword(t *testing.T) {
cc types.CredentialsSet
}
tests := []struct {
name string
args args
wantErr bool
name string
args args
rval bool
}{
{
name: "empty set",
wantErr: true,
args: args{}},
name: "empty set",
rval: false,
args: args{}},
{
name: "bad pwd",
wantErr: true,
name: "bad pwd",
rval: false,
args: args{
password: " foo ",
cc: types.CredentialsSet{&types.Credentials{ID: 1, Credentials: string(hashedPassword)}}}},
{
name: "invalid credentials",
wantErr: true,
name: "invalid credentials",
rval: false,
args: args{
password: " foo ",
cc: types.CredentialsSet{&types.Credentials{ID: 0, Credentials: string(hashedPassword)}}}},
{
name: "ok",
wantErr: false,
name: "ok",
rval: true,
args: args{
password: plainPassword,
cc: types.CredentialsSet{&types.Credentials{ID: 1, Credentials: string(hashedPassword)}}}},
{
name: "multipass",
wantErr: false,
name: "multipass",
rval: true,
args: args{
password: plainPassword,
cc: types.CredentialsSet{
@ -210,14 +206,13 @@ func Test_auth_checkPassword(t *testing.T) {
}
svc := auth{
logger: zap.NewNop(),
settings: &types.Settings{},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := svc.checkPassword(tt.args.password, tt.args.cc); (err != nil) != tt.wantErr {
t.Errorf("auth.checkPassword() error = %v, wantErr %v", err, tt.wantErr)
if tt.rval != svc.checkPassword(tt.args.password, tt.args.cc) {
t.Errorf("auth.checkPassword() expecting rval to be %v", tt.rval)
}
})
}
@ -232,43 +227,34 @@ func Test_auth_validateToken(t *testing.T) {
args args
wantID uint64
wantCredentials string
wantErr bool
}{
// TODO: Add test cases.
{
name: "empty",
wantID: 0,
wantCredentials: "",
wantErr: true,
args: args{token: ""}},
{
name: "foo",
wantID: 0,
wantCredentials: "",
wantErr: true,
args: args{token: "foo1"}},
{
name: "semivalid",
wantID: 0,
wantCredentials: "",
wantErr: true,
args: args{token: "foofoofoofoofoofoofoofoofoofoofo0"}},
{
name: "valid",
wantID: 1,
wantCredentials: "foofoofoofoofoofoofoofoofoofoofo",
wantErr: false,
args: args{token: "foofoofoofoofoofoofoofoofoofoofo1"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := auth{}
svc.logger = zap.NewNop()
gotID, gotCredentials, err := svc.validateToken(tt.args.token)
if (err != nil) != tt.wantErr {
t.Errorf("auth.validateToken() error = %v, wantErr %v", err, tt.wantErr)
return
}
gotID, gotCredentials := svc.validateToken(tt.args.token)
if gotID != tt.wantID {
t.Errorf("auth.validateToken() gotID = %v, want %v", gotID, tt.wantID)
}

View File

@ -2,32 +2,26 @@ package service
import (
"context"
"fmt"
"strconv"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
"github.com/cortezaproject/corteza-server/pkg/handle"
"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/service/event"
"github.com/cortezaproject/corteza-server/system/types"
)
const (
ErrRoleNameNotUnique = serviceError("RoleNameNotUnique")
ErrRoleHandleNotUnique = serviceError("RoleHandleNotUnique")
)
type (
role struct {
db *factory.DB
ctx context.Context
logger *zap.Logger
db *factory.DB
ctx context.Context
actionlog actionlog.Recorder
ac roleAccessController
eventbus eventDispatcher
@ -79,8 +73,10 @@ func Role(ctx context.Context) RoleService {
return (&role{
ac: DefaultAccessControl,
eventbus: eventbus.Service(),
logger: DefaultLogger.Named("role"),
user: DefaultUser.With(ctx),
actionlog: DefaultActionlog,
user: DefaultUser.With(ctx),
}).With(ctx)
}
@ -90,7 +86,8 @@ func (svc role) With(ctx context.Context) RoleService {
db: db,
ctx: ctx,
logger: svc.logger,
actionlog: svc.actionlog,
ac: svc.ac,
eventbus: svc.eventbus,
user: svc.user,
@ -99,90 +96,116 @@ func (svc role) With(ctx context.Context) RoleService {
}
}
func (svc role) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger {
return logger.AddRequestID(ctx, svc.logger).With(fields...)
func (svc role) Find(filter types.RoleFilter) (rr types.RoleSet, f types.RoleFilter, err error) {
var (
raProps = &roleActionProps{filter: &filter}
)
err = svc.db.Transaction(func() error {
filter.IsReadable = svc.ac.FilterReadableRoles(svc.ctx)
if filter.Deleted > 0 {
// If list with deleted or suspended users is requested
// user must have access permissions to system (ie: is admin)
//
// not the best solution but ATM it allows us to have at least
// some kind of control over who can see deleted or archived roles
if !svc.ac.CanAccess(svc.ctx) {
return RoleErrNotAllowedToListRoles()
}
}
rr, f, err = svc.role.Find(filter)
return err
})
return rr, f, svc.recordAction(svc.ctx, raProps, RoleActionSearch, err)
}
func (svc role) FindByID(roleID uint64) (*types.Role, error) {
return svc.findByID(roleID)
func (svc role) FindByID(roleID uint64) (r *types.Role, err error) {
var (
raProps = &roleActionProps{role: &types.Role{ID: roleID}}
)
err = svc.db.Transaction(func() error {
r, err = svc.findByID(roleID)
raProps.setRole(r)
return err
})
return r, svc.recordAction(svc.ctx, raProps, RoleActionLookup, err)
}
func (svc role) findByID(roleID uint64) (*types.Role, error) {
if roleID == 0 {
return nil, ErrInvalidID.withStack()
return nil, RoleErrInvalidID()
}
role, err := svc.role.FindByID(roleID)
if err != nil {
return nil, err
}
if !svc.ac.CanReadRole(svc.ctx, role) {
return nil, ErrNoPermissions.withStack()
}
return role, nil
return svc.role.FindByID(roleID)
}
func (svc role) Find(f types.RoleFilter) (types.RoleSet, types.RoleFilter, error) {
f.IsReadable = svc.ac.FilterReadableRoles(svc.ctx)
func (svc role) FindByName(name string) (r *types.Role, err error) {
var (
raProps = &roleActionProps{role: &types.Role{Name: name}}
)
if f.Deleted > 0 {
// If list with deleted or suspended users is requested
// user must have access permissions to system (ie: is admin)
//
// not the best solution but ATM it allows us to have at least
// some kind of control over who can see deleted or archived roles
if !svc.ac.CanAccess(svc.ctx) {
return nil, f, ErrNoPermissions.withStack()
}
}
err = svc.db.Transaction(func() error {
r, err = svc.role.FindByName(name)
raProps.setRole(r)
return err
})
return svc.role.Find(f)
return r, svc.recordAction(svc.ctx, raProps, RoleActionLookup, err)
}
func (svc role) FindByName(rolename string) (*types.Role, error) {
return svc.role.FindByName(rolename)
}
func (svc role) FindByHandle(h string) (r *types.Role, err error) {
var (
raProps = &roleActionProps{role: &types.Role{Handle: h}}
)
func (svc role) FindByHandle(handle string) (*types.Role, error) {
return svc.role.FindByHandle(handle)
err = svc.db.Transaction(func() error {
r, err = svc.role.FindByName(h)
raProps.setRole(r)
return err
})
return r, svc.recordAction(svc.ctx, raProps, RoleActionLookup, err)
}
// FindByAny finds role by given identifier (id, handle, name)
func (svc role) FindByAny(identifier interface{}) (r *types.Role, err error) {
if ID, ok := identifier.(uint64); ok {
r, err = svc.FindByID(ID)
return svc.FindByID(ID)
} else if strIdentifier, ok := identifier.(string); ok {
if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 {
r, err = svc.FindByID(ID)
return svc.FindByID(ID)
} else {
r, err = svc.FindByHandle(strIdentifier)
if err == nil && r.ID == 0 {
r, err = svc.FindByName(strIdentifier)
return svc.FindByName(strIdentifier)
}
return r, err
}
} else {
err = ErrInvalidID.withStack()
return nil, RoleErrInvalidID()
}
if err != nil {
return
}
return
}
func (svc role) Create(new *types.Role) (r *types.Role, err error) {
var (
raProps = &roleActionProps{new: new}
)
if !handle.IsValid(new.Handle) {
return nil, ErrInvalidHandle
}
err = svc.db.Transaction(func() (err error) {
if !handle.IsValid(new.Handle) {
return RoleErrInvalidHandle()
}
if !svc.ac.CanCreateRole(svc.ctx) {
return nil, ErrNoCreatePermissions.withStack()
}
if !svc.ac.CanCreateRole(svc.ctx) {
return RoleErrNotAllowedToCreate()
}
return r, svc.db.Transaction(func() (err error) {
if err = svc.eventbus.WaitFor(svc.ctx, event.RoleBeforeCreate(new, r)); err != nil {
return
}
@ -195,29 +218,40 @@ func (svc role) Create(new *types.Role) (r *types.Role, err error) {
return
}
raProps.setRole(r)
defer svc.eventbus.Dispatch(svc.ctx, event.RoleAfterCreate(new, r))
return
})
return r, svc.recordAction(svc.ctx, raProps, RoleActionCreate, err)
}
func (svc role) Update(upd *types.Role) (r *types.Role, err error) {
if upd.ID == 0 {
return nil, ErrInvalidID.withStack()
}
var (
raProps = &roleActionProps{update: upd}
)
if !handle.IsValid(upd.Handle) {
return nil, ErrInvalidHandle.withStack()
}
err = svc.db.Transaction(func() (err error) {
if upd.ID == 0 {
return RoleErrInvalidID()
}
if !svc.ac.CanUpdateRole(svc.ctx, upd) {
return nil, ErrNoUpdatePermissions.withStack()
}
if !handle.IsValid(upd.Handle) {
return RoleErrInvalidHandle()
}
if !svc.ac.CanUpdateRole(svc.ctx, upd) {
return RoleErrNotAllowedToUpdate()
}
return r, svc.db.Transaction(func() (err error) {
if r, err = svc.role.FindByID(upd.ID); err != nil {
return
}
raProps.setRole(r)
if err = svc.eventbus.WaitFor(svc.ctx, event.RoleBeforeUpdate(upd, r)); err != nil {
return
}
@ -226,10 +260,10 @@ func (svc role) Update(upd *types.Role) (r *types.Role, err error) {
return
}
// Assign changed values
r.Name = upd.Name
r.Handle = upd.Handle
r.Name = upd.Name
// Assign changed values
if r, err = svc.role.Update(r); err != nil {
return err
}
@ -238,18 +272,26 @@ func (svc role) Update(upd *types.Role) (r *types.Role, err error) {
return nil
})
return r, svc.recordAction(svc.ctx, raProps, RoleActionUpdate, err)
}
func (svc role) UniqueCheck(r *types.Role) (err error) {
var (
raProps = &roleActionProps{role: r}
)
if r.Handle != "" {
if ex, _ := svc.role.FindByHandle(r.Handle); ex != nil && ex.ID > 0 && ex.ID != r.ID {
return ErrRoleHandleNotUnique
raProps.setExisting(ex)
return RoleErrHandleNotUnique()
}
}
if r.Name != "" {
if ex, _ := svc.role.FindByName(r.Name); ex != nil && ex.ID > 0 && ex.ID != r.ID {
return ErrRoleNameNotUnique
raProps.setExisting(ex)
return RoleErrNameNotUnique()
}
}
@ -258,178 +300,300 @@ func (svc role) UniqueCheck(r *types.Role) (err error) {
func (svc role) Delete(roleID uint64) (err error) {
var (
role *types.Role
r *types.Role
raProps = &roleActionProps{role: &types.Role{ID: roleID}}
)
if role, err = svc.findByID(roleID); err != nil {
return err
}
err = svc.db.Transaction(func() (err error) {
if r, err = svc.findByID(roleID); err != nil {
return err
}
if !svc.ac.CanDeleteRole(svc.ctx, role) {
return ErrNoPermissions.withStack()
}
raProps.setRole(r)
if !svc.ac.CanDeleteRole(svc.ctx, r) {
return RoleErrNotAllowedToDelete()
}
if err = svc.eventbus.WaitFor(svc.ctx, event.RoleBeforeDelete(nil, r)); err != nil {
return
}
if err = svc.role.DeleteByID(roleID); err != nil {
return
}
defer svc.eventbus.Dispatch(svc.ctx, event.RoleAfterDelete(nil, r))
if err = svc.eventbus.WaitFor(svc.ctx, event.RoleBeforeDelete(nil, role)); err != nil {
return
}
})
return svc.recordAction(svc.ctx, raProps, RoleActionDelete, err)
}
func (svc role) Undelete(roleID uint64) (err error) {
var (
r *types.Role
raProps = &roleActionProps{role: &types.Role{ID: roleID}}
)
err = svc.db.Transaction(func() (err error) {
if r, err = svc.findByID(roleID); err != nil {
return err
}
raProps.setRole(r)
if !svc.ac.CanDeleteRole(svc.ctx, r) {
return RoleErrNotAllowedToDelete()
}
if err = svc.role.UndeleteByID(roleID); err != nil {
return
}
return nil
})
return svc.recordAction(svc.ctx, raProps, RoleActionUndelete, err)
}
func (svc role) Archive(roleID uint64) (err error) {
var (
r *types.Role
raProps = &roleActionProps{role: &types.Role{ID: roleID}}
)
err = svc.db.Transaction(func() (err error) {
if r, err = svc.findByID(roleID); err != nil {
return err
}
raProps.setRole(r)
if !svc.ac.CanUpdateRole(svc.ctx, r) {
return RoleErrNotAllowedToArchive()
}
if err = svc.role.ArchiveByID(roleID); err != nil {
return
}
if err = svc.role.DeleteByID(roleID); err != nil {
return
}
})
defer svc.eventbus.Dispatch(svc.ctx, event.RoleAfterDelete(nil, role))
return
return svc.recordAction(svc.ctx, raProps, RoleActionArchive, err)
}
func (svc role) Undelete(roleID uint64) error {
role, err := svc.findByID(roleID)
if err != nil {
return err
}
func (svc role) Unarchive(roleID uint64) (err error) {
var (
r *types.Role
raProps = &roleActionProps{role: &types.Role{ID: roleID}}
)
if !svc.ac.CanDeleteRole(svc.ctx, role) {
return ErrNoPermissions.withStack()
}
err = svc.db.Transaction(func() (err error) {
if r, err = svc.findByID(roleID); err != nil {
return err
}
return svc.role.UndeleteByID(roleID)
raProps.setRole(r)
if !svc.ac.CanDeleteRole(svc.ctx, r) {
return RoleErrNotAllowedToDelete()
}
if err = svc.role.UndeleteByID(roleID); err != nil {
return
}
return nil
})
return svc.recordAction(svc.ctx, raProps, RoleActionUnarchive, err)
}
func (svc role) Archive(roleID uint64) error {
role, err := svc.findByID(roleID)
if err != nil {
return err
}
func (svc role) Merge(roleID, targetRoleID uint64) (err error) {
var (
r *types.Role
t *types.Role
if !svc.ac.CanUpdateRole(svc.ctx, role) {
return ErrNoPermissions.withStack()
}
raProps = &roleActionProps{
role: &types.Role{ID: roleID},
target: &types.Role{ID: targetRoleID},
}
)
return svc.role.ArchiveByID(roleID)
}
func (svc role) Unarchive(roleID uint64) error {
role, err := svc.findByID(roleID)
if err != nil {
return err
}
if !svc.ac.CanUpdateRole(svc.ctx, role) {
return ErrNoPermissions.withStack()
}
return svc.role.UnarchiveByID(roleID)
}
func (svc role) Merge(roleID, targetRoleID uint64) error {
role, err := svc.findByID(roleID)
if err != nil {
return err
}
if targetRoleID == 0 {
return ErrInvalidID.withStack()
}
if !svc.ac.CanUpdateRole(svc.ctx, role) {
return ErrNoPermissions.withStack()
}
return svc.role.MergeByID(roleID, targetRoleID)
err = svc.db.Transaction(func() (err error) {
if roleID == 0 || targetRoleID == 0 {
return RoleErrInvalidID()
}
if r, err = svc.findByID(roleID); err != nil {
return err
}
raProps.setRole(r)
if !svc.ac.CanUpdateRole(svc.ctx, r) {
return RoleErrNotAllowedToUpdate()
}
if t, err = svc.findByID(targetRoleID); err != nil {
return err
}
raProps.setTarget(t)
if !svc.ac.CanUpdateRole(svc.ctx, t) {
return RoleErrNotAllowedToUpdate()
}
if err = svc.role.MergeByID(roleID, targetRoleID); err != nil {
return
}
return nil
})
return svc.recordAction(svc.ctx, raProps, RoleActionMerge, err)
}
// Move
//
// @obsolete
func (svc role) Move(roleID, targetOrganisationID uint64) error {
role, err := svc.findByID(roleID)
if err != nil {
return err
}
if targetOrganisationID == 0 {
return ErrInvalidID.withStack()
}
if !svc.ac.CanUpdateRole(svc.ctx, role) {
return ErrNoPermissions.withStack()
}
return svc.role.MoveByID(roleID, targetOrganisationID)
return RoleErrGeneric().Wrap(fmt.Errorf("obsolete"))
}
func (svc role) Membership(userID uint64) ([]*types.RoleMember, error) {
return svc.role.MembershipsFindByUserID(userID)
}
func (svc role) MemberList(roleID uint64) ([]*types.RoleMember, error) {
if roleID == permissions.EveryoneRoleID {
return nil, ErrInvalidID.withStack()
}
_, err := svc.findByID(roleID)
if err != nil {
return nil, err
}
return svc.role.MemberFindByRoleID(roleID)
}
func (svc role) MemberAdd(roleID, userID uint64) (err error) {
func (svc role) MemberList(roleID uint64) (mm []*types.RoleMember, err error) {
var (
role *types.Role
user *types.User
r *types.Role
raProps = &roleActionProps{
role: &types.Role{ID: roleID},
}
)
if role, err = svc.findByID(roleID); err != nil {
return
}
err = svc.db.Transaction(func() error {
if roleID == permissions.EveryoneRoleID || roleID == 0 {
return RoleErrInvalidID()
}
if user, err = svc.user.FindByID(userID); err != nil {
return
}
if r, err = svc.findByID(roleID); err != nil {
return err
}
if err = svc.eventbus.WaitFor(svc.ctx, event.RoleMemberBeforeAdd(user, role)); err != nil {
return
}
if !svc.ac.CanReadRole(svc.ctx, r) {
return RoleErrNotAllowedToRead()
}
if !svc.ac.CanManageRoleMembers(svc.ctx, role) {
return errors.New("Not allowed to manage role members")
}
if mm, err = svc.role.MemberFindByRoleID(roleID); err != nil {
return err
}
if err = svc.role.MemberAddByID(role.ID, user.ID); err != nil {
return
}
return nil
})
defer svc.eventbus.Dispatch(svc.ctx, event.RoleMemberAfterAdd(user, role))
return nil
return mm, svc.recordAction(svc.ctx, raProps, RoleActionMembers, err)
}
func (svc role) MemberRemove(roleID, userID uint64) (err error) {
// MemberAdd adds member (user) to a role
func (svc role) MemberAdd(roleID, memberID uint64) (err error) {
var (
role *types.Role
user *types.User
r *types.Role
m *types.User
raProps = &roleActionProps{
role: &types.Role{ID: roleID},
member: &types.User{ID: memberID},
}
)
if role, err = svc.findByID(roleID); err != nil {
return
}
err = svc.db.Transaction(func() (err error) {
if roleID == permissions.EveryoneRoleID || roleID == 0 || memberID == 0 {
return RoleErrInvalidID()
}
if user, err = svc.user.FindByID(userID); err != nil {
return
}
if r, err = svc.findByID(roleID); err != nil {
return
}
if err = svc.eventbus.WaitFor(svc.ctx, event.RoleMemberBeforeRemove(user, role)); err != nil {
return
}
raProps.setRole(r)
if !svc.ac.CanManageRoleMembers(svc.ctx, role) {
return errors.New("Not allowed to manage role members")
}
if m, err = svc.user.FindByID(memberID); err != nil {
return
}
if err = svc.role.MemberRemoveByID(role.ID, user.ID); err != nil {
return
}
raProps.setMember(m)
defer svc.eventbus.Dispatch(svc.ctx, event.RoleMemberAfterRemove(user, role))
return nil
if err = svc.eventbus.WaitFor(svc.ctx, event.RoleMemberBeforeAdd(m, r)); err != nil {
return
}
if !svc.ac.CanManageRoleMembers(svc.ctx, r) {
return RoleErrNotAllowedToManageMembers()
}
if err = svc.role.MemberAddByID(r.ID, m.ID); err != nil {
return
}
defer svc.eventbus.Dispatch(svc.ctx, event.RoleMemberAfterAdd(m, r))
return nil
})
return svc.recordAction(svc.ctx, raProps, RoleActionMemberAdd, err)
}
// MemberRemove removes member (user) from a role
func (svc role) MemberRemove(roleID, memberID uint64) (err error) {
var (
r *types.Role
m *types.User
raProps = &roleActionProps{
role: &types.Role{ID: roleID},
member: &types.User{ID: memberID},
}
)
err = svc.db.Transaction(func() (err error) {
if roleID == permissions.EveryoneRoleID || roleID == 0 || memberID == 0 {
return RoleErrInvalidID()
}
if r, err = svc.findByID(roleID); err != nil {
return
}
raProps.setRole(r)
if m, err = svc.user.FindByID(memberID); err != nil {
return
}
raProps.setMember(m)
if err = svc.eventbus.WaitFor(svc.ctx, event.RoleMemberBeforeRemove(m, r)); err != nil {
return
}
if !svc.ac.CanManageRoleMembers(svc.ctx, r) {
return RoleErrNotAllowedToManageMembers()
}
if err = svc.role.MemberRemoveByID(r.ID, m.ID); err != nil {
return
}
defer svc.eventbus.Dispatch(svc.ctx, event.RoleMemberAfterRemove(m, r))
return nil
})
return svc.recordAction(svc.ctx, raProps, RoleActionMemberRemove, err)
}
var _ RoleService = &role{}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,135 @@
# List of loggable service actions
resource: system:role
service: role
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: alert
import:
- github.com/cortezaproject/corteza-server/system/types
props:
- name: member
type: "*types.User"
fields: [ handle, email, name, ID ]
- name: role
type: "*types.Role"
fields: [ handle, name, ID ]
- name: new
type: "*types.Role"
fields: [ handle, name, ID ]
- name: update
type: "*types.Role"
fields: [ handle, name, ID ]
- name: existing
type: "*types.Role"
fields: [ handle, name, ID ]
- name: target
type: "*types.Role"
fields: [ handle, name, ID ]
- name: filter
type: "*types.RoleFilter"
fields: [ query, roleID, memberID, handle, name, deleted, archived, sort ]
actions:
- action: search
log: "searched for roles"
severity: info
- action: lookup
log: "looked-up for a {role}"
severity: info
- action: create
log: "created {role}"
- action: update
log: "updated {role}"
- action: delete
log: "deleted {role}"
- action: undelete
log: "undeleted {role}"
- action: archive
log: "archived {role}"
- action: unarchive
log: "unarchived {role}"
- action: merge
log: "merged {target} with {role}"
- action: members
log: "searched for members of {role}"
- action: memberAdd
log: "added {member.email} to {role}"
- action: memberRemove
log: "removed {member.email} from {role}"
errors:
- error: nonexistent
message: "role does not exist"
severity: warning
- error: invalidID
message: "invalid ID"
severity: warning
- error: invalidHandle
message: "invalid handle"
severity: warning
- error: notAllowedToRead
message: "not allowed to read role"
log: "failed to read {role.handle}; insufficient permissions"
- error: notAllowedToListRoles
message: "not allowed to list roles"
log: "failed to list role; insufficient permissions"
- error: notAllowedToCreate
message: "not allowed to create role"
log: "failed to create role; insufficient permissions"
- error: notAllowedToUpdate
message: "not allowed to update role"
log: "failed to update {role.handle}; insufficient permissions"
- error: notAllowedToDelete
message: "not allowed to delete role"
log: "failed to delete {role.handle}; insufficient permissions"
- error: notAllowedToUndelete
message: "not allowed to undelete role"
log: "failed to undelete {role.handle}; insufficient permissions"
- error: notAllowedToArchive
message: "not allowed to archive role"
log: "failed to archive {role.handle}; insufficient permissions"
- error: notAllowedToUnarchive
message: "not allowed to unarchive role"
log: "failed to unarchive {role.handle}; insufficient permissions"
- error: notAllowedToManageMembers
message: "not allowed to manage role members"
log: "failed to manage {role.handle} members; insufficient permissions"
- error: handleNotUnique
message: "role handle not unique"
log: "used duplicate handle ({role.handle}) for role"
severity: warning
- error: nameNotUnique
message: "role name not unique"
log: "used duplicate name ({role.name}) for role"
severity: warning

View File

@ -2,17 +2,19 @@ package service
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/store"
"github.com/cortezaproject/corteza-server/pkg/store/minio"
"github.com/cortezaproject/corteza-server/pkg/store/plain"
"go.uber.org/zap"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
actionlogRepository "github.com/cortezaproject/corteza-server/pkg/actionlog/repository"
"github.com/cortezaproject/corteza-server/pkg/app/options"
intAuth "github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
"github.com/cortezaproject/corteza-server/pkg/permissions"
"github.com/cortezaproject/corteza-server/pkg/settings"
"github.com/cortezaproject/corteza-server/pkg/store"
"github.com/cortezaproject/corteza-server/pkg/store/minio"
"github.com/cortezaproject/corteza-server/pkg/store/plain"
"github.com/cortezaproject/corteza-server/system/repository"
"github.com/cortezaproject/corteza-server/system/types"
)
@ -74,6 +76,8 @@ var (
// CurrentSettings represents current system settings
CurrentSettings = &types.Settings{}
DefaultActionlog actionlog.Recorder
DefaultSink *sink
DefaultAuth AuthService
@ -90,6 +94,12 @@ var (
func Initialize(ctx context.Context, log *zap.Logger, c Config) (err error) {
DefaultLogger = log.Named("service")
DefaultActionlog = actionlog.NewService(
actionlogRepository.Mysql(repository.DB(ctx), "sys_actionlog"),
log,
log,
)
if DefaultPermissions == nil {
// Do not override permissions service stored under DefaultPermissions
// to allow integration tests to inject own permission service

View File

@ -2,45 +2,36 @@ package service
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/handle"
"github.com/cortezaproject/corteza-server/pkg/rh"
"io"
"net/mail"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
internalAuth "github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
"github.com/cortezaproject/corteza-server/pkg/logger"
"github.com/cortezaproject/corteza-server/pkg/handle"
"github.com/cortezaproject/corteza-server/pkg/permissions"
"github.com/cortezaproject/corteza-server/pkg/rh"
"github.com/cortezaproject/corteza-server/system/repository"
"github.com/cortezaproject/corteza-server/system/service/event"
"github.com/cortezaproject/corteza-server/system/types"
)
const (
ErrUserInvalidCredentials = serviceError("UserInvalidCredentials")
ErrUserHandleNotUnique = serviceError("UserHandleNotUnique")
ErrUserUsernameNotUnique = serviceError("UserUsernameNotUnique")
ErrUserEmailNotUnique = serviceError("UserEmailNotUnique")
ErrUserInvalidEmail = serviceError("UserInvalidEmail")
ErrUserLocked = serviceError("UserLocked")
maskPrivateDataEmail = "####.#######@######.###"
maskPrivateDataName = "##### ##########"
)
type (
user struct {
db *factory.DB
ctx context.Context
logger *zap.Logger
db *factory.DB
ctx context.Context
actionlog actionlog.Recorder
settings *types.Settings
@ -56,7 +47,7 @@ type (
}
userAuth interface {
checkPasswordStrength(string) error
checkPasswordStrength(string) bool
changePassword(uint64, string) error
}
@ -79,6 +70,10 @@ type (
FilterUsersWithUnmaskableName(ctx context.Context) *permissions.ResourceFilter
}
// Temp types to support user.Preloader
userIdGetter func(chan uint64)
userSetter func(*types.User) error
UserService interface {
With(ctx context.Context) UserService
@ -101,33 +96,32 @@ type (
Undelete(id uint64) error
SetPassword(userID uint64, password string) error
Preloader(userIdGetter, types.UserFilter, userSetter) error
}
)
func User(ctx context.Context) UserService {
return (&user{
logger: DefaultLogger.Named("user"),
eventbus: eventbus.Service(),
ac: DefaultAccessControl,
settings: CurrentSettings,
auth: DefaultAuth,
actionlog: DefaultActionlog,
subscription: CurrentSubscription,
}).With(ctx)
}
// log() returns zap's logger with requestID from current context and fields.
func (svc user) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger {
return logger.AddRequestID(ctx, svc.logger).With(fields...)
}
func (svc user) With(ctx context.Context) UserService {
db := repository.DB(ctx)
return &user{
ctx: ctx,
db: db,
logger: svc.logger,
ctx: ctx,
db: db,
actionlog: svc.actionlog,
ac: svc.ac,
eventbus: svc.eventbus,
@ -141,33 +135,70 @@ func (svc user) With(ctx context.Context) UserService {
}
}
func (svc user) FindByID(ID uint64) (*types.User, error) {
if ID == 0 {
return nil, ErrInvalidID.withStack()
}
func (svc user) FindByID(userID uint64) (u *types.User, err error) {
var (
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
tmp := internalAuth.NewIdentity(ID)
if internalAuth.IsSuperUser(tmp) {
// Handling case when looking for a super-user
//
// Currently, superuser is a virtual entity
// and does not exists in the db
return &types.User{ID: ID}, nil
}
err = svc.db.Transaction(func() error {
if userID == 0 {
return UserErrInvalidID()
}
return svc.proc(svc.user.FindByID(ID))
su := internalAuth.NewIdentity(userID)
if internalAuth.IsSuperUser(su) {
// Handling case when looking for a super-user
//
// Currently, superuser is a virtual entity
// and does not exists in the db
u = &types.User{ID: userID}
return nil
}
u, err = svc.proc(svc.user.FindByID(userID))
return err
})
return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err)
}
func (svc user) FindByEmail(email string) (*types.User, error) {
return svc.proc(svc.user.FindByEmail(email))
func (svc user) FindByEmail(email string) (u *types.User, err error) {
var (
uaProps = &userActionProps{user: &types.User{Email: email}}
)
err = svc.db.Transaction(func() error {
u, err = svc.proc(svc.user.FindByEmail(email))
return err
})
return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err)
}
func (svc user) FindByUsername(username string) (*types.User, error) {
return svc.proc(svc.user.FindByUsername(username))
func (svc user) FindByUsername(username string) (u *types.User, err error) {
var (
uaProps = &userActionProps{user: &types.User{Username: username}}
)
err = svc.db.Transaction(func() error {
u, err = svc.proc(svc.user.FindByUsername(username))
return err
})
return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err)
}
func (svc user) FindByHandle(handle string) (*types.User, error) {
return svc.proc(svc.user.FindByHandle(handle))
func (svc user) FindByHandle(handle string) (u *types.User, err error) {
var (
uaProps = &userActionProps{user: &types.User{Handle: handle}}
)
err = svc.db.Transaction(func() error {
u, err = svc.proc(svc.user.FindByHandle(handle))
return err
})
return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err)
}
// FindByAny finds user by given identifier (context, id, handle, email)
@ -189,7 +220,7 @@ func (svc user) FindByAny(identifier interface{}) (u *types.User, err error) {
u, err = svc.FindByHandle(strIdentifier)
}
} else {
err = ErrInvalidID.withStack()
err = UserErrInvalidID()
}
if err != nil {
@ -215,74 +246,80 @@ func (svc user) proc(u *types.User, err error) (*types.User, error) {
return u, nil
}
func (svc user) Find(f types.UserFilter) (types.UserSet, types.UserFilter, error) {
if f.Deleted > 0 {
// If list with deleted users is requested
// user must have access permissions to system (ie: is admin)
//
// not the best solution but ATM it allows us to have at least
// some kind of control over who can see deleted users
if !svc.ac.CanAccess(svc.ctx) {
return nil, f, ErrNoPermissions.withStack()
func (svc user) Find(filter types.UserFilter) (uu types.UserSet, f types.UserFilter, err error) {
var (
uaProps = &userActionProps{filter: &filter}
)
err = svc.db.Transaction(func() error {
if filter.Deleted > 0 {
// If list with deleted users is requested
// user must have access permissions to system (ie: is admin)
//
// not the best solution but ATM it allows us to have at least
// some kind of control over who can see deleted users
if !svc.ac.CanAccess(svc.ctx) {
return UserErrNotAllowedToListUsers()
}
}
}
// Prepare filter for email unmasking check
f.IsEmailUnmaskable = svc.ac.FilterUsersWithUnmaskableEmail(svc.ctx)
// Prepare filter for email unmasking check
filter.IsEmailUnmaskable = svc.ac.FilterUsersWithUnmaskableEmail(svc.ctx)
// Prepare filter for name unmasking check
f.IsNameUnmaskable = svc.ac.FilterUsersWithUnmaskableName(svc.ctx)
// Prepare filter for name unmasking check
filter.IsNameUnmaskable = svc.ac.FilterUsersWithUnmaskableName(svc.ctx)
f.IsReadable = svc.ac.FilterReadableUsers(svc.ctx)
filter.IsReadable = svc.ac.FilterReadableUsers(svc.ctx)
return svc.procSet(svc.user.Find(f))
}
uu, f, err = svc.user.Find(filter)
if err != nil {
return err
}
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 uu.Walk(func(u *types.User) error {
svc.handlePrivateData(u)
return nil
})
})
return u, f, nil
return uu, f, svc.recordAction(svc.ctx, uaProps, UserActionSearch, err)
}
func (svc user) Create(new *types.User) (u *types.User, err error) {
if !svc.ac.CanCreateUser(svc.ctx) {
return nil, ErrNoCreatePermissions.withStack()
}
var (
uaProps = &userActionProps{new: new}
)
if !handle.IsValid(new.Handle) {
return nil, ErrInvalidHandle.withStack()
}
if _, err := mail.ParseAddress(new.Email); err != nil {
return nil, ErrUserInvalidEmail.withStack()
}
if svc.subscription != nil {
// When we have an active subscription, we need to check
// if users can be creare or did this deployment hit
// it's user-limit
err = svc.subscription.CanCreateUser(svc.user.Total())
if err != nil {
return nil, err
err = svc.db.Transaction(func() (err error) {
if !svc.ac.CanCreateUser(svc.ctx) {
return UserErrNotAllowedToCreate()
}
}
if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeCreate(new, u)); err != nil {
return
}
if !handle.IsValid(new.Handle) {
return UserErrInvalidHandle()
}
if new.Handle == "" {
createHandle(svc.user, new)
}
if _, err := mail.ParseAddress(new.Email); err != nil {
return UserErrInvalidEmail()
}
return u, svc.db.Transaction(func() (err error) {
if svc.subscription != nil {
// When we have an active subscription, we need to check
// if users can be create or did this deployment hit
// it's user-limit
err = svc.subscription.CanCreateUser(svc.user.Total())
if err != nil {
return err
}
}
if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeCreate(new, u)); err != nil {
return
}
if new.Handle == "" {
createHandle(svc.user, new)
}
if err = svc.UniqueCheck(new); err != nil {
return
@ -295,6 +332,8 @@ func (svc user) Create(new *types.User) (u *types.User, err error) {
defer svc.eventbus.Dispatch(svc.ctx, event.UserAfterCreate(new, u))
return
})
return u, svc.recordAction(svc.ctx, uaProps, UserActionCreate, err)
}
func (svc user) CreateWithAvatar(input *types.User, avatar io.Reader) (out *types.User, err error) {
@ -303,40 +342,46 @@ func (svc user) CreateWithAvatar(input *types.User, avatar io.Reader) (out *type
}
func (svc user) Update(upd *types.User) (u *types.User, err error) {
if upd.ID == 0 {
return nil, ErrInvalidID.withStack()
}
var (
uaProps = &userActionProps{update: upd}
)
if !handle.IsValid(upd.Handle) {
return nil, ErrInvalidHandle.withStack()
}
if _, err := mail.ParseAddress(upd.Email); err != nil {
return nil, ErrUserInvalidEmail.withStack()
}
if u, err = svc.user.FindByID(upd.ID); err != nil {
return
}
if upd.ID != internalAuth.GetIdentityFromContext(svc.ctx).Identity() {
if !svc.ac.CanUpdateUser(svc.ctx, u) {
return nil, ErrNoUpdatePermissions.withStack()
err = svc.db.Transaction(func() (err error) {
if upd.ID == 0 {
return ErrInvalidID
}
}
// Assign changed values
u.Email = upd.Email
u.Username = upd.Username
u.Name = upd.Name
u.Handle = upd.Handle
u.Kind = upd.Kind
if !handle.IsValid(upd.Handle) {
return UserErrInvalidHandle()
}
if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeUpdate(upd, u)); err != nil {
return
}
if _, err := mail.ParseAddress(upd.Email); err != nil {
return UserErrInvalidEmail()
}
if u, err = svc.user.FindByID(upd.ID); err != nil {
return
}
uaProps.setUser(u)
if upd.ID != internalAuth.GetIdentityFromContext(svc.ctx).Identity() {
if !svc.ac.CanUpdateUser(svc.ctx, u) {
return UserErrNotAllowedToUpdate()
}
}
// Assign changed values
u.Email = upd.Email
u.Username = upd.Username
u.Name = upd.Name
u.Handle = upd.Handle
u.Kind = upd.Kind
if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeUpdate(upd, u)); err != nil {
return
}
return u, svc.db.Transaction(func() (err error) {
if err = svc.UniqueCheck(u); err != nil {
return
}
@ -348,8 +393,11 @@ func (svc user) Update(upd *types.User) (u *types.User, err error) {
defer svc.eventbus.Dispatch(svc.ctx, event.UserAfterUpdate(upd, u))
return
})
return u, svc.recordAction(svc.ctx, uaProps, UserActionUpdate, err)
}
// UniqueCheck verifies user's email, username and handle
func (svc user) UniqueCheck(u *types.User) (err error) {
isUnique := func(field string) bool {
f := types.UserFilter{
@ -392,15 +440,15 @@ func (svc user) UniqueCheck(u *types.User) (err error) {
}
if !isUnique("email") {
return ErrUserEmailNotUnique
return UserErrEmailNotUnique()
}
if !isUnique("username") {
return ErrUserUsernameNotUnique
return UserErrUsernameNotUnique()
}
if !isUnique("handle") {
return ErrUserHandleNotUnique
return UserErrHandleNotUnique()
}
return nil
@ -411,128 +459,173 @@ func (svc user) UpdateWithAvatar(mod *types.User, avatar io.Reader) (out *types.
return svc.Create(mod)
}
func (svc user) Delete(ID uint64) (err error) {
func (svc user) Delete(userID uint64) (err error) {
var (
del *types.User
u *types.User
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
if ID == 0 {
return ErrInvalidID.withStack()
}
err = svc.db.Transaction(func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if del, err = svc.user.FindByID(ID); err != nil {
return
}
if u, err = svc.user.FindByID(userID); err != nil {
return
}
if !svc.ac.CanDeleteUser(svc.ctx, del) {
return ErrNoPermissions.withStack()
}
if !svc.ac.CanDeleteUser(svc.ctx, u) {
return UserErrNotAllowedToDelete()
}
if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeUpdate(nil, del)); err != nil {
return
}
if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeUpdate(nil, u)); err != nil {
return
}
if err = svc.user.DeleteByID(ID); err != nil {
return
}
if err = svc.user.DeleteByID(userID); err != nil {
return
}
defer svc.eventbus.Dispatch(svc.ctx, event.UserAfterDelete(nil, del))
return
defer svc.eventbus.Dispatch(svc.ctx, event.UserAfterDelete(nil, u))
return nil
})
return svc.recordAction(svc.ctx, uaProps, UserActionDelete, err)
}
func (svc user) Undelete(ID uint64) (err error) {
if ID == 0 {
return ErrInvalidID.withStack()
}
func (svc user) Undelete(userID uint64) (err error) {
var (
u *types.User
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
var u *types.User
if u, err = svc.user.FindByID(ID); err != nil {
return
}
err = svc.db.Transaction(func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if err = svc.UniqueCheck(u); err != nil {
return err
}
if u, err = svc.user.FindByID(userID); err != nil {
return
}
if !svc.ac.CanDeleteUser(svc.ctx, u) {
return ErrNoPermissions.withStack()
}
uaProps.setUser(u)
return svc.db.Transaction(func() (err error) {
return svc.user.UndeleteByID(ID)
if err = svc.UniqueCheck(u); err != nil {
return err
}
if !svc.ac.CanDeleteUser(svc.ctx, u) {
return UserErrNotAllowedToDelete()
}
if err = svc.user.UndeleteByID(userID); err != nil {
return err
}
return nil
})
return svc.recordAction(svc.ctx, uaProps, UserActionUndelete, err)
}
func (svc user) Suspend(ID uint64) (err error) {
if ID == 0 {
return ErrInvalidID.withStack()
}
func (svc user) Suspend(userID uint64) (err error) {
var (
u *types.User
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
var u *types.User
if u, err = svc.user.FindByID(ID); err != nil {
return
}
err = svc.db.Transaction(func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if !svc.ac.CanSuspendUser(svc.ctx, u) {
return ErrNoPermissions.withStack()
}
if u, err = svc.user.FindByID(userID); err != nil {
return
}
return svc.db.Transaction(func() (err error) {
return svc.user.SuspendByID(ID)
uaProps.setUser(u)
if !svc.ac.CanSuspendUser(svc.ctx, u) {
return UserErrNotAllowedToSuspend()
}
if err = svc.user.SuspendByID(userID); err != nil {
return err
}
return nil
})
return svc.recordAction(svc.ctx, uaProps, UserActionSuspend, err)
}
func (svc user) Unsuspend(ID uint64) (err error) {
if ID == 0 {
return ErrInvalidID.withStack()
}
func (svc user) Unsuspend(userID uint64) (err error) {
var (
u *types.User
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
var u *types.User
if u, err = svc.user.FindByID(ID); err != nil {
return
}
err = svc.db.Transaction(func() (err error) {
if userID == 0 {
return UserErrInvalidID()
}
if !svc.ac.CanUnsuspendUser(svc.ctx, u) {
return ErrNoPermissions.withStack()
}
if u, err = svc.user.FindByID(userID); err != nil {
return
}
return svc.db.Transaction(func() (err error) {
return svc.user.UnsuspendByID(ID)
uaProps.setUser(u)
if !svc.ac.CanUnsuspendUser(svc.ctx, u) {
return UserErrNotAllowedToUnsuspend()
}
if err = svc.user.UnsuspendByID(userID); err != nil {
return err
}
return nil
})
return svc.recordAction(svc.ctx, uaProps, UserActionUnsuspend, err)
}
// SetPassword sets new password for a user
//
// Expecting setter to have permissions to update modify users and internal authentication enabled
func (svc user) SetPassword(userID uint64, newPassword string) (err error) {
log := svc.log(svc.ctx, zap.Uint64("userID", userID))
var (
u *types.User
uaProps = &userActionProps{user: &types.User{ID: userID}}
)
if !svc.settings.Auth.Internal.Enabled {
return errors.New("internal authentication disabled")
}
err = svc.db.Transaction(func() error {
if u, err = svc.user.FindByID(userID); err != nil {
return err
}
var u *types.User
if u, err = svc.user.FindByID(userID); err != nil {
return
}
uaProps.setUser(u)
if !svc.ac.CanUpdateUser(svc.ctx, u) {
return ErrNoPermissions.withStack()
}
if !svc.ac.CanUpdateUser(svc.ctx, u) {
return UserErrNotAllowedToUpdate()
}
if err = svc.auth.checkPasswordStrength(newPassword); err != nil {
return
}
if !svc.auth.checkPasswordStrength(newPassword) {
return UserErrPasswordNotSecure()
}
return svc.db.Transaction(func() error {
if err := svc.auth.changePassword(userID, newPassword); err != nil {
return err
}
log.Info("password changed")
return nil
})
return svc.recordAction(svc.ctx, uaProps, UserActionSetPassword, err)
}
// Masks (or leaves as-is) private data on user
@ -546,6 +639,56 @@ func (svc user) handlePrivateData(u *types.User) {
}
}
// Preloader collects all ids of users, loads them and sets them back
//
//
// @todo this kind of preloader is useful and can be implemented in bunch
// of places and replace old code
func (svc user) Preloader(g userIdGetter, f types.UserFilter, s userSetter) error {
var (
// channel that will collect the IDs in the getter
ch = make(chan uint64, 0)
// unique index for IDs
unq = make(map[uint64]bool)
)
// Reset the collection of the IDs
f.UserID = make([]uint64, 0)
// Call getter and collect the IDs
go g(ch)
rangeLoop:
for {
select {
case <-svc.ctx.Done():
close(ch)
break rangeLoop
case id, ok := <-ch:
if !ok {
// Channel closed
break rangeLoop
}
if !unq[id] {
unq[id] = true
f.UserID = append(f.UserID, id)
}
}
}
// Load all users (even if deleted, suspended) from the given list of IDs
uu, _, err := svc.Find(f)
if err != nil {
return err
}
return uu.Walk(s)
}
func createHandle(r repository.UserRepository, u *types.User) {
if u.Handle == "" {
u.Handle, _ = handle.Cast(

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,129 @@
# List of loggable service actions
resource: system:user
service: user
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: alert
import:
- github.com/cortezaproject/corteza-server/system/types
props:
- name: user
type: "*types.User"
fields: [ handle, email, name, username, ID ]
- name: new
type: "*types.User"
fields: [ handle, email, name, username, ID ]
- name: update
type: "*types.User"
fields: [ handle, email, name, username, ID ]
- name: existing
type: "*types.User"
fields: [ handle, email, name, username, ID ]
- name: filter
type: "*types.UserFilter"
fields: [ query, userID, roleID, handle, email, username, deleted, suspended, sort ]
actions:
- action: search
log: "searched for matching users"
severity: info
- action: lookup
log: "looked-up for a {user}"
severity: info
- action: create
log: "created {user}"
- action: update
log: "updated {user}"
- action: delete
log: "deleted {user}"
- action: undelete
log: "undeleted {user}"
- action: suspend
log: "suspended {user}"
- action: unsuspend
log: "unsuspended {user}"
- action: setPassword
log: "password changed for {user}"
errors:
- error: nonexistent
message: "user does not exist"
severity: warning
- error: invalidID
message: "invalid ID"
severity: warning
- error: invalidHandle
message: "invalid handle"
severity: warning
- error: invalidEmail
message: "invalid email"
severity: warning
- error: notAllowedToRead
message: "not allowed to read user"
log: "failed to read {user.handle}; insufficient permissions"
- error: notAllowedToListUsers
message: "not allowed to list users"
log: "failed to list user; insufficient permissions"
- error: notAllowedToCreate
message: "not allowed to create user"
log: "failed to create user; insufficient permissions"
- error: notAllowedToUpdate
message: "not allowed to update user"
log: "failed to update {user.handle}; insufficient permissions"
- error: notAllowedToDelete
message: "not allowed to delete user"
log: "failed to delete {user.handle}; insufficient permissions"
- error: notAllowedToUndelete
message: "not allowed to undelete user"
log: "failed to undelete {user.handle}; insufficient permissions"
- error: notAllowedToSuspend
message: "not allowed to suspend user"
log: "failed to suspend {user.handle}; insufficient permissions"
- error: notAllowedToUnsuspend
message: "not allowed to unsuspend user"
log: "failed to unsuspend {user.handle}; insufficient permissions"
- error: handleNotUnique
message: "handle not unique"
log: "used duplicate handle ({user.handle}) for user"
severity: warning
- error: emailNotUnique
message: "email not unique"
log: "used duplicate email ({user.email}) for user"
severity: warning
- error: usernameNotUnique
message: "username not unique"
log: "used duplicate username ({user.username}) for user"
severity: warning
- error: passwordNotSecure
message: "provided password is not secure; use longer password with more non-alphanumeric character"

View File

@ -68,7 +68,7 @@ func TestRoleList(t *testing.T) {
End()
}
func TestRoleList_filterForbiden(t *testing.T) {
func TestRoleList_filterForbidden(t *testing.T) {
h := newHelper(t)
// @todo this can be a problematic test because it leaves
@ -98,7 +98,7 @@ func TestRoleCreateForbidden(t *testing.T) {
FormData("name", rs()).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("system.service.NoCreatePermissions")).
Assert(helpers.AssertError("not allowed to create role")).
End()
}
@ -113,7 +113,7 @@ func TestRoleCreateNotUnique(t *testing.T) {
FormData("handle", role.Handle).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("system.service.RoleHandleNotUnique")).
Assert(helpers.AssertError("role handle not unique")).
End()
h.apiInit().
@ -122,7 +122,7 @@ func TestRoleCreateNotUnique(t *testing.T) {
FormData("handle", "handle_"+rs()).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("system.service.RoleNameNotUnique")).
Assert(helpers.AssertError("role name not unique")).
End()
}
@ -150,7 +150,7 @@ func TestRoleUpdateForbidden(t *testing.T) {
FormData("email", h.randEmail()).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("system.service.NoUpdatePermissions")).
Assert(helpers.AssertError("not allowed to update role")).
End()
}
@ -186,7 +186,7 @@ func TestRoleDeleteForbidden(t *testing.T) {
Delete(fmt.Sprintf("/roles/%d", u.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("system.service.NoPermissions")).
Assert(helpers.AssertError("not allowed to delete role")).
End()
}

View File

@ -245,7 +245,7 @@ func TestUserCreateForbidden(t *testing.T) {
FormData("email", h.randEmail()).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("system.service.NoCreatePermissions")).
Assert(helpers.AssertError("not allowed to create user")).
End()
}
@ -278,7 +278,7 @@ func TestUserUpdateForbidden(t *testing.T) {
FormData("email", h.randEmail()).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("system.service.NoUpdatePermissions")).
Assert(helpers.AssertError("not allowed to update user")).
End()
}
@ -311,7 +311,7 @@ func TestUserDeleteForbidden(t *testing.T) {
Delete(fmt.Sprintf("/users/%d", u.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("system.service.NoPermissions")).
Assert(helpers.AssertError("not allowed to delete user")).
End()
}