3
0

Add user credentials management endpoints

This commit is contained in:
Denis Arh
2022-08-01 15:19:47 +02:00
parent 1b4a03827c
commit 54060f5ff8
11 changed files with 757 additions and 27 deletions
+1 -2
View File
@@ -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
+17
View File
@@ -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
+55 -17
View File
@@ -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)
})
+101
View File
@@ -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{}
+15
View File
@@ -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.
+21 -8
View File
@@ -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{
+86
View File
@@ -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)
}
+416
View File
@@ -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
}
+42
View File
@@ -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"
+2
View File
@@ -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())
+1
View File
@@ -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"
}
}