3
0

Add basic impersonation support for admins

This commit is contained in:
Denis Arh 2020-08-12 18:39:28 +02:00
parent 4d887ba486
commit a57866bcab
10 changed files with 279 additions and 0 deletions

View File

@ -19,6 +19,22 @@
"path": "/check",
"parameters": {}
},
{
"name": "impersonate",
"method": "POST",
"title": "Impersonate a user",
"path": "/impersonate",
"parameters": {
"post": [
{
"name": "userID",
"type": "uint64",
"required": true,
"title": "ID of the impersonated user"
}
]
}
},
{
"name": "exchangeAuthToken",
"method": "POST",

View File

@ -21,6 +21,28 @@
"Path": "/check",
"Parameters": {}
},
{
"Name": "impersonate",
"Method": "POST",
"Title": "Impersonate a user",
"Path": "/impersonate",
"Parameters": {
"post": [
{
"name": "userID",
"required": true,
"title": "ID of the impersonated user",
"type": "uint64"
},
{
"name": "expire",
"required": false,
"title": "expiration time in seconds, must be shorter than 24h",
"type": "int"
}
]
}
},
{
"Name": "exchangeAuthToken",
"Method": "POST",

View File

@ -946,6 +946,7 @@ Compose records
| --------- | ---- | ------ | ----------- | ------- | --------- |
| filter | string | GET | Filtering condition | N/A | NO |
| fields | []string | GET | Fields to export | N/A | YES |
| timezone | string | GET | Convert times to this timezone | N/A | NO |
| filename | string | PATH | Filename to use | N/A | NO |
| ext | string | PATH | Export format | N/A | YES |
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |

View File

@ -254,6 +254,7 @@ Warning: implode(): Invalid arguments passed in /private/tmp/Users/darh/Work.cru
| ------ | -------- | ------- |
| `GET` | `/auth/` | Returns auth settings |
| `GET` | `/auth/check` | Check JWT token |
| `POST` | `/auth/impersonate` | Impersonate a user |
| `POST` | `/auth/exchange` | Exchange auth token for JWT |
| `GET` | `/auth/logout` | Logout |
@ -283,6 +284,21 @@ Warning: implode(): Invalid arguments passed in /private/tmp/Users/darh/Work.cru
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
## Impersonate a user
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/auth/impersonate` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| userID | uint64 | POST | ID of the impersonated user | N/A | YES |
| expire | int | POST | expiration time in seconds, must be shorter than 24h | N/A | NO |
## Exchange auth token for JWT
#### Method

View File

@ -68,6 +68,18 @@ func (ctrl *Auth) Logout(ctx context.Context, r *request.AuthLogout) (interface{
return true, nil
}
// Impersonate implements impersonation functionality
//
// This is experimental and internals will most likely change in the future:
func (ctrl *Auth) Impersonate(ctx context.Context, r *request.AuthImpersonate) (interface{}, error) {
u, err := ctrl.authSvc.With(ctx).Impersonate(r.UserID)
if err != nil {
return nil, err
}
return ctrl.makePayload(ctx, u)
}
func (ctrl *Auth) Settings(ctx context.Context, r *request.AuthSettings) (interface{}, error) {
var (
int = ctrl.settings.Auth.Internal

View File

@ -31,6 +31,7 @@ import (
type AuthAPI interface {
Settings(context.Context, *request.AuthSettings) (interface{}, error)
Check(context.Context, *request.AuthCheck) (interface{}, error)
Impersonate(context.Context, *request.AuthImpersonate) (interface{}, error)
ExchangeAuthToken(context.Context, *request.AuthExchangeAuthToken) (interface{}, error)
Logout(context.Context, *request.AuthLogout) (interface{}, error)
}
@ -39,6 +40,7 @@ type AuthAPI interface {
type Auth struct {
Settings func(http.ResponseWriter, *http.Request)
Check func(http.ResponseWriter, *http.Request)
Impersonate func(http.ResponseWriter, *http.Request)
ExchangeAuthToken func(http.ResponseWriter, *http.Request)
Logout func(http.ResponseWriter, *http.Request)
}
@ -85,6 +87,26 @@ func NewAuth(h AuthAPI) *Auth {
resputil.JSON(w, value)
}
},
Impersonate: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAuthImpersonate()
if err := params.Fill(r); err != nil {
logger.LogParamError("Auth.Impersonate", r, err)
resputil.JSON(w, err)
return
}
value, err := h.Impersonate(r.Context(), params)
if err != nil {
logger.LogControllerError("Auth.Impersonate", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Auth.Impersonate", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
ExchangeAuthToken: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAuthExchangeAuthToken()
@ -133,6 +155,7 @@ func (h Auth) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.H
r.Use(middlewares...)
r.Get("/auth/", h.Settings)
r.Get("/auth/check", h.Check)
r.Post("/auth/impersonate", h.Impersonate)
r.Post("/auth/exchange", h.ExchangeAuthToken)
r.Get("/auth/logout", h.Logout)
})

View File

@ -128,6 +128,76 @@ func (r *AuthCheck) Fill(req *http.Request) (err error) {
var _ RequestFiller = NewAuthCheck()
// AuthImpersonate request parameters
type AuthImpersonate struct {
hasUserID bool
rawUserID string
UserID uint64 `json:",string"`
hasExpire bool
rawExpire string
Expire int
}
// NewAuthImpersonate request
func NewAuthImpersonate() *AuthImpersonate {
return &AuthImpersonate{}
}
// Auditable returns all auditable/loggable parameters
func (r AuthImpersonate) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["userID"] = r.UserID
out["expire"] = r.Expire
return out
}
// Fill processes request and fills internal variables
func (r *AuthImpersonate) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := post["userID"]; ok {
r.hasUserID = true
r.rawUserID = val
r.UserID = parseUInt64(val)
}
if val, ok := post["expire"]; ok {
r.hasExpire = true
r.rawExpire = val
r.Expire = parseInt(val)
}
return err
}
var _ RequestFiller = NewAuthImpersonate()
// AuthExchangeAuthToken request parameters
type AuthExchangeAuthToken struct {
hasToken bool
@ -237,6 +307,36 @@ func (r *AuthLogout) Fill(req *http.Request) (err error) {
var _ RequestFiller = NewAuthLogout()
// HasUserID returns true if userID was set
func (r *AuthImpersonate) HasUserID() bool {
return r.hasUserID
}
// RawUserID returns raw value of userID parameter
func (r *AuthImpersonate) RawUserID() string {
return r.rawUserID
}
// GetUserID returns casted value of userID parameter
func (r *AuthImpersonate) GetUserID() uint64 {
return r.UserID
}
// HasExpire returns true if expire was set
func (r *AuthImpersonate) HasExpire() bool {
return r.hasExpire
}
// RawExpire returns raw value of expire parameter
func (r *AuthImpersonate) RawExpire() string {
return r.rawExpire
}
// GetExpire returns casted value of expire parameter
func (r *AuthImpersonate) GetExpire() int {
return r.Expire
}
// HasToken returns true if token was set
func (r *AuthExchangeAuthToken) HasToken() bool {
return r.hasToken

View File

@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/slice"
"regexp"
"strconv"
"time"
@ -50,6 +51,7 @@ type (
InternalLogin(email string, password string) (*types.User, error)
SetPassword(userID uint64, AuthActionPassword string) error
ChangePassword(userID uint64, oldPassword, AuthActionPassword string) error
Impersonate(userID uint64) (*types.User, error)
IssueAuthRequestToken(user *types.User) (token string, err error)
ValidateAuthRequestToken(token string) (user *types.User, err error)
@ -595,6 +597,36 @@ func (svc auth) SetPassword(userID uint64, password string) (err error) {
return svc.recordAction(svc.ctx, aam, AuthActionChangePassword, err)
}
// Impersonate verifies if user can impersonate another user and returns that user
//
// For now, it's the caller's responsibility to generate the auth token
func (svc auth) Impersonate(userID uint64) (u *types.User, err error) {
var (
aam = &authActionProps{user: u}
identity = internalAuth.GetIdentityFromContext(svc.ctx)
)
err = func() error {
if !slice.HasUint64(identity.Roles(), permissions.AdminsRoleID) {
// A primitive access control
//
// For now, we do not want to add or change RBAC operation-set
// and we just check if user that wants to impersonate someone
// is member of administrators
return AuthErrNotAllowedToImpersonate()
}
if u, err = svc.users.FindByID(userID); err != nil {
return err
}
return err
}()
return u, svc.recordAction(svc.ctx, aam, AuthActionImpersonate, err)
}
// ChangePassword validates old password and changes it with new
func (svc auth) ChangePassword(userID uint64, oldPassword, AuthActionPassword string) (err error) {
var (

View File

@ -615,6 +615,26 @@ func AuthActionCreateCredentials(props ...*authActionProps) *authAction {
return a
}
// AuthActionImpersonate returns "system:auth.impersonate" error
//
// This function is auto-generated.
//
func AuthActionImpersonate(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
resource: "system:auth",
action: "impersonate",
log: "impersonating {user}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
@ -1163,6 +1183,36 @@ func AuthErrInvalidToken(props ...*authActionProps) *authError {
}
// AuthErrNotAllowedToImpersonate returns "system:auth.notAllowedToImpersonate" audit event as actionlog.Warning
//
//
// This function is auto-generated.
//
func AuthErrNotAllowedToImpersonate(props ...*authActionProps) *authError {
var e = &authError{
timestamp: time.Now(),
resource: "system:auth",
error: "notAllowedToImpersonate",
action: "error",
message: "not allowed to impersonate this user",
log: "not allowed to impersonate this user",
severity: actionlog.Warning,
props: func() *authActionProps {
if len(props) > 0 {
return props[0]
}
return nil
}(),
}
if len(props) > 0 {
e.props = props[0]
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************

View File

@ -65,6 +65,9 @@ actions:
- action: createCredentials
log: "new credentials {credentials.kind} created"
- action: impersonate
log: "impersonating {user}"
errors:
- error: subscription
message: "{err}"
@ -131,3 +134,7 @@ errors:
- error: invalidToken
message: "invalid token"
severity: warning
- error: notAllowedToImpersonate
message: "not allowed to impersonate this user"
severity: warning