Skip to content

Commit

Permalink
feat: Terminal access for trigger roles (#1128)
Browse files Browse the repository at this point in the history
* wip

* wip - added method for updating terminal access policy for old users

* added default policy update method, fixed ci trigger bug, added api for terminal access update

* wip

* fixed sql query

* fixed sql script

* fixed sql script - default auth role

* updated auth for trigger ci api

* wip - ci trigger bug fix

* reverted ci trigger changes

* wip

* wip - rbac for terminal access removed

* fixed policy json error

* wip - added rbac back for update terminal access and ci trigger apis

* ci trigger fix + update policy api fix

* wip

* fixed casbin policy update input

* fix sql query for getting roles

* wip

* wip

* wip

* added own KeyMatch func

* updated keyMatch func

* formatted code

* fixed condition

* review changes

* updated conf file for rbac matcher func update

* updated key match func

* updated trigger policy for terminal object

* review changes

* added rbac in terminal api for charts
  • Loading branch information
kartik-579 authored Feb 8, 2022
1 parent 7a4e7ee commit f799aa4
Show file tree
Hide file tree
Showing 18 changed files with 1,119 additions and 122 deletions.
28 changes: 24 additions & 4 deletions api/restHandler/ArgoApplicationRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ func (impl ArgoApplicationRestHandlerImpl) GetTerminalSession(w http.ResponseWri
return
}
request.AppId = id
//below method is for getting new object, i.e. team/env/app for new trigger policy
teamEnvRbacObject := impl.enforcerUtil.GetTeamEnvRBACNameByAppId(id, eId)
if teamEnvRbacObject == "" {
common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
return
}
//below methods are for getting old objects for old policies (admin, manager roles)
appRbacObject := impl.enforcerUtil.GetAppRBACNameByAppId(id)
if appRbacObject == "" {
common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
Expand All @@ -126,11 +133,24 @@ func (impl ArgoApplicationRestHandlerImpl) GetTerminalSession(w http.ResponseWri
return
}
request.EnvironmentId = eId
if ok := impl.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreate, appRbacObject); !ok {
common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
return
valid := false

//checking if the user has access of terminal with new trigger policy, if not then will check old rbac
if ok := impl.enforcer.Enforce(token, casbin.ResourceTerminal, casbin.ActionExec, teamEnvRbacObject); !ok {
appRbacOk := impl.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreate, appRbacObject)
envRbacOk := impl.enforcer.Enforce(token, casbin.ResourceEnvironment, casbin.ActionCreate, envRbacObject)
if appRbacOk && envRbacOk{
valid = true
}
} else{
valid = true
}
//checking rbac for charts
if ok := impl.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionCreate, teamEnvRbacObject); ok {
valid = true
}
if ok := impl.enforcer.Enforce(token, casbin.ResourceEnvironment, casbin.ActionCreate, envRbacObject); !ok {
//if both the new rbac(trigger access) and old rbac fails then user is forbidden to access terminal
if !valid {
common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
return
}
Expand Down
51 changes: 29 additions & 22 deletions api/restHandler/app/BuildPipelineRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,32 +232,21 @@ func (handler PipelineConfigRestHandlerImpl) TriggerCiPipeline(w http.ResponseWr
handler.Logger.Infow("request payload, TriggerCiPipeline", "payload", ciTriggerRequest)

//RBAC CHECK CD PIPELINE - FOR USER
pipelines, err := handler.pipelineRepository.FindAutomaticByCiPipelineId(ciTriggerRequest.PipelineId)
var authorizedPipelines []pipelineConfig.Pipeline
var unauthorizedPipelines []pipelineConfig.Pipeline
//fetching user only for getting token
triggeredBy, err := handler.userAuthService.GetById(ciTriggerRequest.TriggeredBy)
if err != nil {
handler.Logger.Errorw("service err, TriggerCiPipeline", "err", err, "payload", ciTriggerRequest)
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
pipelines, err := handler.pipelineRepository.FindByCiPipelineId(ciTriggerRequest.PipelineId)
if err!= nil{
handler.Logger.Errorw("error in finding ccd pipelines by ciPipelineId", "err", err)
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
return
}
token := triggeredBy.AccessToken
var authorizedPipelines []pipelineConfig.Pipeline
var unauthorizedPipelines []pipelineConfig.Pipeline
token := r.Header.Get("token")
for _, p := range pipelines {
pass := 0
object := handler.enforcerUtil.GetAppRBACNameByAppId(p.AppId)
if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionTrigger, object); !ok {
handler.Logger.Debug(fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
} else {
pass = 1
}
appRbacOk := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionTrigger, object)
object = handler.enforcerUtil.GetAppRBACByAppIdAndPipelineId(p.AppId, p.Id)
if ok := handler.enforcer.Enforce(token, casbin.ResourceEnvironment, casbin.ActionTrigger, object); !ok {
handler.Logger.Debug(fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
} else {
pass = 2
}
if pass == 2 {
envRbacOk := handler.enforcer.Enforce(token, casbin.ResourceEnvironment, casbin.ActionTrigger, object)
if appRbacOk && envRbacOk {
authorizedPipelines = append(authorizedPipelines, *p)
} else {
unauthorizedPipelines = append(unauthorizedPipelines, *p)
Expand All @@ -266,14 +255,32 @@ func (handler PipelineConfigRestHandlerImpl) TriggerCiPipeline(w http.ResponseWr
resMessage := "allowed for all pipelines"
response := make(map[string]string)
if len(unauthorizedPipelines) > 0 {
resMessage = "not authorized for few pipelines, will not effected"
resMessage = "some pipelines not authorized"
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusForbidden)
return
}
if len(authorizedPipelines) == 0{
//user has no cd pipeline
ciPipeline, err := handler.ciPipelineRepository.FindById(ciTriggerRequest.PipelineId)
if err != nil {
handler.Logger.Errorw("err in finding ci pipeline, TriggerCiPipeline", "err", err, "ciPipelineId", ciTriggerRequest.PipelineId)
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
return
}
object := handler.enforcerUtil.GetAppRBACNameByAppId(ciPipeline.AppId)
if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionTrigger, object); !ok {
handler.Logger.Debug(fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusForbidden)
return
}
}
//RBAC CHECK CD PIPELINE - FOR USER

resp, err := handler.ciHandler.HandleCIManual(ciTriggerRequest)
if err != nil {
handler.Logger.Errorw("service err, TriggerCiPipeline", "err", err, "payload", ciTriggerRequest)
common.WriteJsonResp(w, err, response, http.StatusInternalServerError)
return
}
response["apiResponse"] = strconv.Itoa(resp)
response["authStatus"] = resMessage
Expand Down
27 changes: 27 additions & 0 deletions api/user/UserRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type UserRestHandler interface {
DeleteRoleGroup(w http.ResponseWriter, r *http.Request)
CheckUserRoles(w http.ResponseWriter, r *http.Request)
SyncOrchestratorToCasbin(w http.ResponseWriter, r *http.Request)
UpdateTriggerPolicyForTerminalAccess(w http.ResponseWriter, r *http.Request)
}

type userNamePassword struct {
Expand Down Expand Up @@ -688,3 +689,29 @@ func (handler UserRestHandlerImpl) SyncOrchestratorToCasbin(w http.ResponseWrite
}
common.WriteJsonResp(w, err, flag, http.StatusOK)
}

func (handler UserRestHandlerImpl) UpdateTriggerPolicyForTerminalAccess(w http.ResponseWriter, r *http.Request) {
userId, err := handler.userService.GetLoggedInUser(r)
if userId == 0 || err != nil {
handler.logger.Errorw("unauthorized user, UpdateTriggerPolicyForTerminalAccess", "userId", userId)
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized)
return
}

// RBAC enforcer applying
token := r.Header.Get("token")
if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*"); !ok {
handler.logger.Errorw("unauthorized user, UpdateTriggerPolicyForTerminalAccess", "userId", userId)
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
return
}
//RBAC enforcer Ends

err = handler.userService.UpdateTriggerPolicyForTerminalAccess()
if err != nil {
handler.logger.Errorw("error in updating trigger policy for terminal access", "err", err)
common.WriteJsonResp(w, err, "", http.StatusInternalServerError)
return
}
common.WriteJsonResp(w, nil, "Trigger policy updated successfully.", http.StatusOK)
}
2 changes: 2 additions & 0 deletions api/user/UserRouter.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,6 @@ func (router UserRouterImpl) InitUserRouter(userAuthRouter *mux.Router) {
HandlerFunc(router.userRestHandler.CheckUserRoles).Methods("GET")
userAuthRouter.Path("/sync/orchestratortocasbin").
HandlerFunc(router.userRestHandler.SyncOrchestratorToCasbin).Methods("GET")
userAuthRouter.Path("/update/trigger/terminal").
HandlerFunc(router.userRestHandler.UpdateTriggerPolicyForTerminalAccess).Methods("PUT")
}
4 changes: 4 additions & 0 deletions api/user/wire_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ var UserWireSet = wire.NewSet(
wire.Bind(new(user.UserAuthService), new(*user.UserAuthServiceImpl)),
repository.NewUserAuthRepositoryImpl,
wire.Bind(new(repository.UserAuthRepository), new(*repository.UserAuthRepositoryImpl)),
repository.NewDefaultAuthPolicyRepositoryImpl,
wire.Bind(new(repository.DefaultAuthPolicyRepository), new(*repository.DefaultAuthPolicyRepositoryImpl)),
repository.NewDefaultAuthRoleRepositoryImpl,
wire.Bind(new(repository.DefaultAuthRoleRepository), new(*repository.DefaultAuthRoleRepositoryImpl)),

NewUserRouterImpl,
wire.Bind(new(UserRouter), new(*UserRouterImpl)),
Expand Down
2 changes: 1 addition & 1 deletion auth_model.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
g = _, _

[matchers]
m = g(r.sub, p.sub) && keyMatch(r.res, p.res) && keyMatch(r.act, p.act) && keyMatch(r.obj, p.obj)
m = g(r.sub, p.sub) && matchKeyByPart(r.res, p.res) && matchKeyByPart(r.act, p.act) && matchKeyByPart(r.obj, p.obj)

2 changes: 1 addition & 1 deletion cmd/external-app/auth_model.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
g = _, _

[matchers]
m = g(r.sub, p.sub) && keyMatch(r.res, p.res) && keyMatch(r.act, p.act) && keyMatch(r.obj, p.obj)
m = g(r.sub, p.sub) && matchKeyByPart(r.res, p.res) && matchKeyByPart(r.act, p.act) && matchKeyByPart(r.obj, p.obj)

10 changes: 10 additions & 0 deletions pkg/user/UserService.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type UserService interface {
IsSuperAdmin(userId int) (bool, error)
GetByIdIncludeDeleted(id int32) (*bean.UserInfo, error)
UserExists(emailId string) bool
UpdateTriggerPolicyForTerminalAccess() (err error)
}

type UserServiceImpl struct {
Expand Down Expand Up @@ -959,3 +960,12 @@ func (impl UserServiceImpl) GetByIdIncludeDeleted(id int32) (*bean.UserInfo, err
}
return response, nil
}

func (impl UserServiceImpl) UpdateTriggerPolicyForTerminalAccess() (err error) {
err = impl.userAuthRepository.UpdateTriggerPolicyForTerminalAccess()
if err != nil {
impl.logger.Errorw("error in updating policy for terminal access to trigger role", "err", err)
return err
}
return nil
}
58 changes: 58 additions & 0 deletions pkg/user/casbin/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ func (e *EnforcerImpl) enforce(enf *casbin.Enforcer, rvals ...interface{}) bool
email = sub
}
rvals[0] = strings.ToLower(email)
//adding our key matching func - MatchKeyFunc, to enforcer
enf.AddFunction("matchKeyByPart", MatchKeyByPartFunc)
return enf.Enforce(rvals...)
}

Expand All @@ -109,5 +111,61 @@ func (e *EnforcerImpl) enforceByEmail(enf *casbin.Enforcer, rvals ...interface{}
if len(rvals) == 0 {
return false
}
//adding our key matching func - MatchKeyFunc, to enforcer
enf.AddFunction("matchKeyByPart", MatchKeyByPartFunc)
return enf.Enforce(rvals...)
}

// MatchKeyByPartFunc is the wrapper of our own customised MatchKeyByPart Func
func MatchKeyByPartFunc(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
name2 := args[1].(string)

return bool(MatchKeyByPart(name1, name2)), nil
}

// MatchKeyByPart checks whether values in key1 matches all values of key2(values are obtained by splitting key by "/")
// For example - key1 = "a/b/c" matches key2 = "a/*/c" but not matches for key2 = "a/*/d"
func MatchKeyByPart(key1 string, key2 string) bool {

if key2 == "*"{
//policy must be for super-admin role or global-env action
//no need to check further
return true
}

key1Vals := strings.Split(key1, "/")
key2Vals := strings.Split(key2, "/")

if (len(key1Vals) != len(key2Vals)) || len(key1Vals) == 0 {
//values in keys should be more than zero and must be equal
return false
}

for i, key2Val := range key2Vals {
key1Val := key1Vals[i]

if key2Val == "" || key1Val == "" {
//empty values are not allowed in any key
return false
} else {
// getting index of "*" in key2, will check values of key1 accordingly
//for example - key2Val = a/bc*/d & key1Val = a/bcd/d, in this case "bc" will be checked in key1Val(upto index of "*")
j := strings.Index(key2Val, "*")
if j == -1 {
if key1Val != key2Val {
return false
}
} else if len(key1Val) > j {
if key1Val[:j] != key2Val[:j] {
return false
}
} else {
if key1Val != key2Val[:j] {
return false
}
}
}
}
return true
}
2 changes: 2 additions & 0 deletions pkg/user/casbin/rbacpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
ResourceUser = "user"
ResourceNotification = "notification"
ResourceTemplate = "template"
ResourceTerminal = "terminal"

ResourceProjects = "projects"
ResourceApplications = "applications"
Expand All @@ -48,4 +49,5 @@ const (
ActionSync = "sync"
ActionTrigger = "trigger"
ActionNotify = "notify"
ActionExec = "exec"
)
75 changes: 75 additions & 0 deletions pkg/user/repository/DefaultAuthPolicyRepository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package repository

import (
"github.com/devtron-labs/devtron/pkg/sql"
"github.com/go-pg/pg"
"go.uber.org/zap"
)

type RoleType string

const (
MANAGER_TYPE RoleType = "manager"
ADMIN_TYPE RoleType = "admin"
TRIGGER_TYPE RoleType = "trigger"
VIEW_TYPE RoleType = "view"
ENTITY_ALL_TYPE RoleType = "entityAll"
ENTITY_VIEW_TYPE RoleType = "entityView"
ENTITY_SPECIFIC_TYPE RoleType = "entitySpecific"
ENTITY_SPECIFIC_ADMIN_TYPE RoleType = "entitySpecificAdmin"
ENTITY_SPECIFIC_VIEW_TYPE RoleType = "entitySpecificView"
ROLE_SPECIFIC_TYPE RoleType = "roleSpecific"
)

type DefaultAuthPolicyRepository interface {
CreatePolicy(policy *DefaultAuthPolicy) (*DefaultAuthPolicy, error)
UpdatePolicyByRoleType(policy string, roleType RoleType) (*DefaultAuthPolicy, error)
GetPolicyByRoleType(roleType RoleType) (policy string, err error)
}

type DefaultAuthPolicy struct {
TableName struct{} `sql:"default_auth_policy" pg:",discard_unknown_columns"`
Id int `sql:"id,pk"`
RoleType string `sql:"role_type,notnull"`
Policy string `sql:"policy,notnull"`
sql.AuditLog
}

type DefaultAuthPolicyRepositoryImpl struct {
dbConnection *pg.DB
logger *zap.SugaredLogger
}

func NewDefaultAuthPolicyRepositoryImpl(dbConnection *pg.DB, logger *zap.SugaredLogger) *DefaultAuthPolicyRepositoryImpl {
return &DefaultAuthPolicyRepositoryImpl{dbConnection: dbConnection, logger: logger}
}

func (impl DefaultAuthPolicyRepositoryImpl) CreatePolicy(policy *DefaultAuthPolicy) (*DefaultAuthPolicy, error) {
err := impl.dbConnection.Insert(policy)
if err != nil {
impl.logger.Error("error in creating auth policy", "err", err)
return policy, err
}
return policy, nil
}

func (impl DefaultAuthPolicyRepositoryImpl) UpdatePolicyByRoleType(policy string, roleType RoleType) (*DefaultAuthPolicy, error) {
var model DefaultAuthPolicy
_, err := impl.dbConnection.Model(&model).Set("policy = ?", policy).
Where("role_type = ?", roleType).Update()
if err != nil {
impl.logger.Error("error in updating auth policy", "err", err)
return &model, err
}
return &model, nil
}

func (impl DefaultAuthPolicyRepositoryImpl) GetPolicyByRoleType(roleType RoleType) (policy string, err error) {
var model DefaultAuthPolicy
err = impl.dbConnection.Model(&model).Where("role_type = ?", roleType).Select()
if err != nil {
impl.logger.Error("error in getting policy by roleType", "err", err, "roleType", roleType)
return "", err
}
return model.Policy, nil
}
Loading

0 comments on commit f799aa4

Please sign in to comment.