Add basic impersonation support for admins
This commit is contained in:
parent
4d887ba486
commit
a57866bcab
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
|
||||
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user