From 54060f5ff80f175d026979e6da1a3ed588e7c517 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Mon, 1 Aug 2022 15:19:47 +0200 Subject: [PATCH] Add user credentials management endpoints --- compose/rest/handlers/record.go | 3 +- system/rest.yaml | 17 + system/rest/handlers/user.go | 72 +++- system/rest/request/user.go | 101 ++++++ system/rest/user.go | 15 + system/service/access_control.gen.go | 29 +- system/service/credentials.go | 86 +++++ system/service/credentials_actions.gen.go | 416 ++++++++++++++++++++++ system/service/credentials_actions.yaml | 42 +++ system/service/service.go | 2 + system/user.cue | 1 + 11 files changed, 757 insertions(+), 27 deletions(-) create mode 100644 system/service/credentials.go create mode 100644 system/service/credentials_actions.gen.go create mode 100644 system/service/credentials_actions.yaml diff --git a/compose/rest/handlers/record.go b/compose/rest/handlers/record.go index a25e14b38..0be051cdf 100644 --- a/compose/rest/handlers/record.go +++ b/compose/rest/handlers/record.go @@ -308,8 +308,7 @@ func NewRecord(h RecordAPI) *Record { return } - value, err := h. - Revisions(r.Context(), params) + value, err := h.Revisions(r.Context(), params) if err != nil { api.Send(w, r, err) return diff --git a/system/rest.yaml b/system/rest.yaml index 895c4f926..a7bbd4f14 100644 --- a/system/rest.yaml +++ b/system/rest.yaml @@ -686,6 +686,23 @@ endpoints: required: true title: ID + - name: listCredentials + method: GET + title: List user's credentials + path: "/{userID}/credentials" + parameters: + path: + - { type: uint64, name: userID, required: true, title: ID } + + - name: deleteCredentials + method: DELETE + title: List user's credentials + path: "/{userID}/credentials/{credentialsID}" + parameters: + path: + - { type: uint64, name: userID, required: true, title: ID } + - { type: uint64, name: credentialsID, required: true, title: Credentials ID } + - name: export method: GET title: Export users diff --git a/system/rest/handlers/user.go b/system/rest/handlers/user.go index 674638155..ae261b71c 100644 --- a/system/rest/handlers/user.go +++ b/system/rest/handlers/user.go @@ -34,29 +34,33 @@ type ( MembershipRemove(context.Context, *request.UserMembershipRemove) (interface{}, error) TriggerScript(context.Context, *request.UserTriggerScript) (interface{}, error) SessionsRemove(context.Context, *request.UserSessionsRemove) (interface{}, error) + ListCredentials(context.Context, *request.UserListCredentials) (interface{}, error) + DeleteCredentials(context.Context, *request.UserDeleteCredentials) (interface{}, error) Export(context.Context, *request.UserExport) (interface{}, error) Import(context.Context, *request.UserImport) (interface{}, error) } // HTTP API interface User struct { - List func(http.ResponseWriter, *http.Request) - Create func(http.ResponseWriter, *http.Request) - Update func(http.ResponseWriter, *http.Request) - PartialUpdate func(http.ResponseWriter, *http.Request) - Read func(http.ResponseWriter, *http.Request) - Delete func(http.ResponseWriter, *http.Request) - Suspend func(http.ResponseWriter, *http.Request) - Unsuspend func(http.ResponseWriter, *http.Request) - Undelete func(http.ResponseWriter, *http.Request) - SetPassword func(http.ResponseWriter, *http.Request) - MembershipList func(http.ResponseWriter, *http.Request) - MembershipAdd func(http.ResponseWriter, *http.Request) - MembershipRemove func(http.ResponseWriter, *http.Request) - TriggerScript func(http.ResponseWriter, *http.Request) - SessionsRemove func(http.ResponseWriter, *http.Request) - Export func(http.ResponseWriter, *http.Request) - Import func(http.ResponseWriter, *http.Request) + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + PartialUpdate func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) + Suspend func(http.ResponseWriter, *http.Request) + Unsuspend func(http.ResponseWriter, *http.Request) + Undelete func(http.ResponseWriter, *http.Request) + SetPassword func(http.ResponseWriter, *http.Request) + MembershipList func(http.ResponseWriter, *http.Request) + MembershipAdd func(http.ResponseWriter, *http.Request) + MembershipRemove func(http.ResponseWriter, *http.Request) + TriggerScript func(http.ResponseWriter, *http.Request) + SessionsRemove func(http.ResponseWriter, *http.Request) + ListCredentials func(http.ResponseWriter, *http.Request) + DeleteCredentials func(http.ResponseWriter, *http.Request) + Export func(http.ResponseWriter, *http.Request) + Import func(http.ResponseWriter, *http.Request) } ) @@ -302,6 +306,38 @@ func NewUser(h UserAPI) *User { api.Send(w, r, value) }, + ListCredentials: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserListCredentials() + if err := params.Fill(r); err != nil { + api.Send(w, r, err) + return + } + + value, err := h.ListCredentials(r.Context(), params) + if err != nil { + api.Send(w, r, err) + return + } + + api.Send(w, r, value) + }, + DeleteCredentials: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserDeleteCredentials() + if err := params.Fill(r); err != nil { + api.Send(w, r, err) + return + } + + value, err := h.DeleteCredentials(r.Context(), params) + if err != nil { + api.Send(w, r, err) + return + } + + api.Send(w, r, value) + }, Export: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewUserExport() @@ -355,6 +391,8 @@ func (h User) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.H r.Delete("/users/{userID}/membership/{roleID}", h.MembershipRemove) r.Post("/users/{userID}/trigger", h.TriggerScript) r.Delete("/users/{userID}/sessions", h.SessionsRemove) + r.Get("/users/{userID}/credentials", h.ListCredentials) + r.Delete("/users/{userID}/credentials/{credentialsID}", h.DeleteCredentials) r.Get("/users/export/{filename}.zip", h.Export) r.Post("/users/import", h.Import) }) diff --git a/system/rest/request/user.go b/system/rest/request/user.go index dc379b46c..00e2137d5 100644 --- a/system/rest/request/user.go +++ b/system/rest/request/user.go @@ -280,6 +280,25 @@ type ( UserID uint64 `json:",string"` } + UserListCredentials struct { + // UserID PATH parameter + // + // ID + UserID uint64 `json:",string"` + } + + UserDeleteCredentials struct { + // UserID PATH parameter + // + // ID + UserID uint64 `json:",string"` + + // CredentialsID PATH parameter + // + // Credentials ID + CredentialsID uint64 `json:",string"` + } + UserExport struct { // Filename PATH parameter // @@ -1420,6 +1439,88 @@ func (r *UserSessionsRemove) Fill(req *http.Request) (err error) { return err } +// NewUserListCredentials request +func NewUserListCredentials() *UserListCredentials { + return &UserListCredentials{} +} + +// Auditable returns all auditable/loggable parameters +func (r UserListCredentials) Auditable() map[string]interface{} { + return map[string]interface{}{ + "userID": r.UserID, + } +} + +// Auditable returns all auditable/loggable parameters +func (r UserListCredentials) GetUserID() uint64 { + return r.UserID +} + +// Fill processes request and fills internal variables +func (r *UserListCredentials) Fill(req *http.Request) (err error) { + + { + var val string + // path params + + val = chi.URLParam(req, "userID") + r.UserID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + } + + return err +} + +// NewUserDeleteCredentials request +func NewUserDeleteCredentials() *UserDeleteCredentials { + return &UserDeleteCredentials{} +} + +// Auditable returns all auditable/loggable parameters +func (r UserDeleteCredentials) Auditable() map[string]interface{} { + return map[string]interface{}{ + "userID": r.UserID, + "credentialsID": r.CredentialsID, + } +} + +// Auditable returns all auditable/loggable parameters +func (r UserDeleteCredentials) GetUserID() uint64 { + return r.UserID +} + +// Auditable returns all auditable/loggable parameters +func (r UserDeleteCredentials) GetCredentialsID() uint64 { + return r.CredentialsID +} + +// Fill processes request and fills internal variables +func (r *UserDeleteCredentials) Fill(req *http.Request) (err error) { + + { + var val string + // path params + + val = chi.URLParam(req, "userID") + r.UserID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + val = chi.URLParam(req, "credentialsID") + r.CredentialsID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + } + + return err +} + // NewUserExport request func NewUserExport() *UserExport { return &UserExport{} diff --git a/system/rest/user.go b/system/rest/user.go index 6f5510909..61d65c101 100644 --- a/system/rest/user.go +++ b/system/rest/user.go @@ -35,6 +35,7 @@ type ( User struct { user service.UserService role service.RoleService + cred userCredentials userAc userAccessController roleAc roleAccessController @@ -45,6 +46,11 @@ type ( Set types.UserSet `json:"set"` } + userCredentials interface { + List(ctx context.Context, userID uint64) (cc types.CredentialSet, err error) + Delete(ctx context.Context, userID, credentialsID uint64) (err error) + } + userAccessController interface { CanCreateUser(context.Context) bool CanUpdateUser(context.Context, *types.User) bool @@ -55,6 +61,7 @@ func (User) New() *User { return &User{ user: service.DefaultUser, role: service.DefaultRole, + cred: service.DefaultCredentials, userAc: service.DefaultAccessControl, roleAc: service.DefaultAccessControl, @@ -278,6 +285,14 @@ func (ctrl *User) SessionsRemove(ctx context.Context, r *request.UserSessionsRem return } +func (ctrl *User) ListCredentials(ctx context.Context, r *request.UserListCredentials) (rsp interface{}, err error) { + return ctrl.cred.List(ctx, r.UserID) +} + +func (ctrl *User) DeleteCredentials(ctx context.Context, r *request.UserDeleteCredentials) (rsp interface{}, err error) { + return true, ctrl.cred.Delete(ctx, r.UserID, r.CredentialsID) +} + // Export exports users with optional role membership and related roles // // @note this is a temporary implementation; it will be reworked when we rework Envoy and related bits. diff --git a/system/service/access_control.gen.go b/system/service/access_control.gen.go index 071d5be88..226078d80 100644 --- a/system/service/access_control.gen.go +++ b/system/service/access_control.gen.go @@ -340,6 +340,11 @@ func (svc accessControl) List() (out []map[string]string) { "any": types.UserRbacResource(0), "op": "impersonate", }, + { + "type": types.UserResourceType, + "any": types.UserRbacResource(0), + "op": "credentials.manage", + }, { "type": types.DalConnectionResourceType, "any": types.DalConnectionRbacResource(0), @@ -851,6 +856,13 @@ func (svc accessControl) CanImpersonateUser(ctx context.Context, r *types.User) return svc.can(ctx, "impersonate", r) } +// CanManageCredentialsOnUser checks if current user can manage user's credentials +// +// This function is auto-generated +func (svc accessControl) CanManageCredentialsOnUser(ctx context.Context, r *types.User) bool { + return svc.can(ctx, "credentials.manage", r) +} + // CanReadDalConnection checks if current user can read connection // // This function is auto-generated @@ -1283,14 +1295,15 @@ func rbacResourceOperations(r string) map[string]bool { } case types.UserResourceType: return map[string]bool{ - "read": true, - "update": true, - "delete": true, - "suspend": true, - "unsuspend": true, - "email.unmask": true, - "name.unmask": true, - "impersonate": true, + "read": true, + "update": true, + "delete": true, + "suspend": true, + "unsuspend": true, + "email.unmask": true, + "name.unmask": true, + "impersonate": true, + "credentials.manage": true, } case types.DalConnectionResourceType: return map[string]bool{ diff --git a/system/service/credentials.go b/system/service/credentials.go new file mode 100644 index 000000000..8122880a0 --- /dev/null +++ b/system/service/credentials.go @@ -0,0 +1,86 @@ +package service + +import ( + "context" + "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/store" + "github.com/cortezaproject/corteza-server/system/types" +) + +type ( + credentials struct { + actionlog actionlog.Recorder + ac credentialsAccessController + store store.Storer + } + + credentialsAccessController interface { + CanManageCredentialsOnUser(context.Context, *types.User) bool + } +) + +func Credentials() *credentials { + return &credentials{ + ac: DefaultAccessControl, + store: DefaultStore, + actionlog: DefaultActionlog, + } +} + +func (svc *credentials) List(ctx context.Context, userID uint64) (cc types.CredentialSet, err error) { + var ( + u *types.User + + caProps = &credentialsActionProps{user: &types.User{ID: userID}} + ) + + err = func() error { + u, err = loadUser(ctx, svc.store, userID) + caProps.setUser(u) + + if !svc.ac.CanManageCredentialsOnUser(ctx, u) { + return CredentialsErrNotAllowedToManage() + } + + cc, _, err = store.SearchCredentials(ctx, svc.store, types.CredentialFilter{OwnerID: u.ID}) + return err + }() + + return cc, svc.recordAction(ctx, caProps, CredentialsActionSearch, err) +} + +func (svc *credentials) Delete(ctx context.Context, userID, credentialsID uint64) (err error) { + var ( + c *types.Credential + u *types.User + caProps = &credentialsActionProps{user: &types.User{ID: userID}} + ) + + err = func() (err error) { + if u, err = loadUser(ctx, svc.store, userID); err != nil { + return + } + + if u.Kind == types.SystemUser { + return CredentialsErrNotAllowedToManage() + } + + if !svc.ac.CanManageCredentialsOnUser(ctx, u) { + return CredentialsErrNotAllowedToManage() + } + + if c, err = store.LookupCredentialByID(ctx, svc.store, credentialsID); err != nil { + return + } + + caProps.setCredentials(c) + c.DeletedAt = now() + if err = store.UpdateCredential(ctx, svc.store, c); err != nil { + return + } + + return nil + }() + + return svc.recordAction(ctx, caProps, CredentialsActionDelete, err) +} diff --git a/system/service/credentials_actions.gen.go b/system/service/credentials_actions.gen.go new file mode 100644 index 000000000..32fb91b78 --- /dev/null +++ b/system/service/credentials_actions.gen.go @@ -0,0 +1,416 @@ +package service + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// system/service/credentials_actions.yaml + +import ( + "context" + "fmt" + "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/pkg/errors" + "github.com/cortezaproject/corteza-server/pkg/locale" + "github.com/cortezaproject/corteza-server/system/types" + "strings" + "time" +) + +type ( + credentialsActionProps struct { + user *types.User + credentials *types.Credential + } + + credentialsAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *credentialsActionProps + } + + credentialsLogMetaKey struct{} + credentialsPropsMetaKey struct{} +) + +var ( + // just a placeholder to cover template cases w/o fmt package use + _ = fmt.Println +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods +// setUser updates credentialsActionProps's user +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *credentialsActionProps) setUser(user *types.User) *credentialsActionProps { + p.user = user + return p +} + +// setCredentials updates credentialsActionProps's credentials +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *credentialsActionProps) setCredentials(credentials *types.Credential) *credentialsActionProps { + p.credentials = credentials + return p +} + +// Serialize converts credentialsActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p credentialsActionProps) Serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + ) + + if p.user != nil { + m.Set("user.handle", p.user.Handle, true) + m.Set("user.email", p.user.Email, true) + m.Set("user.name", p.user.Name, true) + m.Set("user.username", p.user.Username, true) + m.Set("user.ID", p.user.ID, true) + } + if p.credentials != nil { + m.Set("credentials.kind", p.credentials.Kind, true) + } + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p credentialsActionProps) Format(in string, err error) string { + var ( + pairs = []string{"{{err}}"} + // first non-empty string + fns = func(ii ...interface{}) string { + for _, i := range ii { + if s := fmt.Sprintf("%v", i); len(s) > 0 { + return s + } + } + + return "" + } + ) + + if err != nil { + pairs = append(pairs, err.Error()) + } else { + pairs = append(pairs, "nil") + } + + if p.user != nil { + // replacement for "{{user}}" (in order how fields are defined) + pairs = append( + pairs, + "{{user}}", + fns( + p.user.Handle, + p.user.Email, + p.user.Name, + p.user.Username, + p.user.ID, + ), + ) + pairs = append(pairs, "{{user.handle}}", fns(p.user.Handle)) + pairs = append(pairs, "{{user.email}}", fns(p.user.Email)) + pairs = append(pairs, "{{user.name}}", fns(p.user.Name)) + pairs = append(pairs, "{{user.username}}", fns(p.user.Username)) + pairs = append(pairs, "{{user.ID}}", fns(p.user.ID)) + } + + if p.credentials != nil { + // replacement for "{{credentials}}" (in order how fields are defined) + pairs = append( + pairs, + "{{credentials}}", + fns( + p.credentials.Kind, + ), + ) + pairs = append(pairs, "{{credentials.kind}}", fns(p.credentials.Kind)) + } + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *credentialsAction) String() string { + var props = &credentialsActionProps{} + + if a.props != nil { + props = a.props + } + + return props.Format(a.log, nil) +} + +func (e *credentialsAction) ToAction() *actionlog.Action { + return &actionlog.Action{ + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Meta: e.props.Serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action constructors + +// CredentialsActionSearch returns "system:credentials.search" action +// +// This function is auto-generated. +// +func CredentialsActionSearch(props ...*credentialsActionProps) *credentialsAction { + a := &credentialsAction{ + timestamp: time.Now(), + resource: "system:credentials", + action: "search", + log: "searched for matching users", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// CredentialsActionDelete returns "system:credentials.delete" action +// +// This function is auto-generated. +// +func CredentialsActionDelete(props ...*credentialsActionProps) *credentialsAction { + a := &credentialsAction{ + timestamp: time.Now(), + resource: "system:credentials", + action: "delete", + log: "deleted {{user}}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// CredentialsErrGeneric returns "system:credentials.generic" as *errors.Error +// +// +// This function is auto-generated. +// +func CredentialsErrGeneric(mm ...*credentialsActionProps) *errors.Error { + var p = &credentialsActionProps{} + if len(mm) > 0 { + p = mm[0] + } + + var e = errors.New( + errors.KindInternal, + + p.Format("failed to complete request due to internal error", nil), + + errors.Meta("type", "generic"), + errors.Meta("resource", "system:credentials"), + + // action log entry; no formatting, it will be applied inside recordAction fn. + errors.Meta(credentialsLogMetaKey{}, "{err}"), + errors.Meta(credentialsPropsMetaKey{}, p), + + // translation namespace & key + errors.Meta(locale.ErrorMetaNamespace{}, "system"), + errors.Meta(locale.ErrorMetaKey{}, "credentials.errors.generic"), + + errors.StackSkip(1), + ) + + if len(mm) > 0 { + } + + return e +} + +// CredentialsErrNotFound returns "system:credentials.notFound" as *errors.Error +// +// +// This function is auto-generated. +// +func CredentialsErrNotFound(mm ...*credentialsActionProps) *errors.Error { + var p = &credentialsActionProps{} + if len(mm) > 0 { + p = mm[0] + } + + var e = errors.New( + errors.KindInternal, + + p.Format("credentials not found", nil), + + errors.Meta("type", "notFound"), + errors.Meta("resource", "system:credentials"), + + errors.Meta(credentialsPropsMetaKey{}, p), + + // translation namespace & key + errors.Meta(locale.ErrorMetaNamespace{}, "system"), + errors.Meta(locale.ErrorMetaKey{}, "credentials.errors.notFound"), + + errors.StackSkip(1), + ) + + if len(mm) > 0 { + } + + return e +} + +// CredentialsErrInvalidID returns "system:credentials.invalidID" as *errors.Error +// +// +// This function is auto-generated. +// +func CredentialsErrInvalidID(mm ...*credentialsActionProps) *errors.Error { + var p = &credentialsActionProps{} + if len(mm) > 0 { + p = mm[0] + } + + var e = errors.New( + errors.KindInternal, + + p.Format("invalid ID", nil), + + errors.Meta("type", "invalidID"), + errors.Meta("resource", "system:credentials"), + + errors.Meta(credentialsPropsMetaKey{}, p), + + // translation namespace & key + errors.Meta(locale.ErrorMetaNamespace{}, "system"), + errors.Meta(locale.ErrorMetaKey{}, "credentials.errors.invalidID"), + + errors.StackSkip(1), + ) + + if len(mm) > 0 { + } + + return e +} + +// CredentialsErrNotAllowedToManage returns "system:credentials.notAllowedToManage" as *errors.Error +// +// +// This function is auto-generated. +// +func CredentialsErrNotAllowedToManage(mm ...*credentialsActionProps) *errors.Error { + var p = &credentialsActionProps{} + if len(mm) > 0 { + p = mm[0] + } + + var e = errors.New( + errors.KindInternal, + + p.Format("not allowed to manage credentials for this user", nil), + + errors.Meta("type", "notAllowedToManage"), + errors.Meta("resource", "system:credentials"), + + // action log entry; no formatting, it will be applied inside recordAction fn. + errors.Meta(credentialsLogMetaKey{}, "failed to manage credentials for {{user.handle}}; insufficient permissions"), + errors.Meta(credentialsPropsMetaKey{}, p), + + // translation namespace & key + errors.Meta(locale.ErrorMetaNamespace{}, "system"), + errors.Meta(locale.ErrorMetaKey{}, "credentials.errors.notAllowedToManage"), + + errors.StackSkip(1), + ) + + if len(mm) > 0 { + } + + return e +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* + +// recordAction is a service helper function wraps function that can return error +// +// It will wrap unrecognized/internal errors with generic errors. +// +// This function is auto-generated. +// +func (svc credentials) recordAction(ctx context.Context, props *credentialsActionProps, actionFn func(...*credentialsActionProps) *credentialsAction, err error) error { + if svc.actionlog == nil || actionFn == nil { + // action log disabled or no action fn passed, return error as-is + return err + } else if err == nil { + // action completed w/o error, record it + svc.actionlog.Record(ctx, actionFn(props).ToAction()) + return nil + } + + a := actionFn(props).ToAction() + + // Extracting error information and recording it as action + a.Error = err.Error() + + switch c := err.(type) { + case *errors.Error: + m := c.Meta() + + a.Error = err.Error() + a.Severity = actionlog.Severity(m.AsInt("severity")) + a.Description = props.Format(m.AsString(credentialsLogMetaKey{}), err) + + if p, has := m[credentialsPropsMetaKey{}]; has { + a.Meta = p.(*credentialsActionProps).Serialize() + } + + svc.actionlog.Record(ctx, a) + default: + svc.actionlog.Record(ctx, a) + } + + // Original error is passed on + return err +} diff --git a/system/service/credentials_actions.yaml b/system/service/credentials_actions.yaml new file mode 100644 index 000000000..a68aa1a2b --- /dev/null +++ b/system/service/credentials_actions.yaml @@ -0,0 +1,42 @@ +# List of loggable service actions + +resource: system:credentials +service: credentials + +# 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: credentials + type: "*types.Credential" + fields: [ kind ] + +actions: + - action: search + log: "searched for matching users" + severity: info + + - action: delete + log: "deleted {{user}}" + +errors: + - error: notFound + message: "credentials not found" + severity: warning + + - error: invalidID + message: "invalid ID" + severity: warning + + - error: notAllowedToManage + message: "not allowed to manage credentials for this user" + log: "failed to manage credentials for {{user.handle}}; insufficient permissions" diff --git a/system/service/service.go b/system/service/service.go index 8fac10d1d..2e151cec4 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -75,6 +75,7 @@ var ( DefaultAuth *auth DefaultAuthClient *authClient DefaultUser *user + DefaultCredentials *credentials DefaultDalConnection *dalConnection DefaultDalSensitivityLevel *dalSensitivityLevel DefaultRole *role @@ -201,6 +202,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock DefaultAuth = Auth(AuthOptions{LimitUsers: c.Limit.SystemUsers}) DefaultAuthClient = AuthClient(DefaultStore, DefaultAccessControl, DefaultActionlog, eventbus.Service(), c.Auth) DefaultUser = User(UserOptions{LimitUsers: c.Limit.SystemUsers}) + DefaultCredentials = Credentials() DefaultReport = Report(DefaultStore, DefaultAccessControl, DefaultActionlog, eventbus.Service()) DefaultRole = Role(rbac.Global()) DefaultApplication = Application(DefaultStore, DefaultAccessControl, DefaultActionlog, eventbus.Service()) diff --git a/system/user.cue b/system/user.cue index 14cb4c173..44c1f9997 100644 --- a/system/user.cue +++ b/system/user.cue @@ -50,6 +50,7 @@ user: schema.#Resource & { "email.unmask": description: "Unmask email" "name.unmask": description: "Unmask name" "impersonate": description: "Impersonate user" + "credentials.manage": description: "Manage user's credentials" } }