fix(apiserver): enforce self_ug job scopes and accept project filters (bug-1934, bug-1935)
bug-1934: every job-permission check read a single scope and compared it only with "self", so a job:view or job:control grant scoped to self_ug acted like "all" (list, get by ID, control), and self + self_ug collapsed to self. userJobScope() now collects all of a user's grants of a job permission before deduplication and adds them up (all; self; self_ug = the user and everyone sharing a user group). The job list appends a submit_user filter for narrower scopes, get-by-ID keeps covered rows in a new slice (removing from the ranged slice skipped rows) and answers 403, both control paths and job output check the owner against the scope. User groups now reach the apiserver: the config push converts cluster_config_t.ugroups and OnUserGroupsUpdated() replaces the new store (nested groups, all, exclusions). The default basic_user now also sees its user-group mates' jobs, as its configuration says. bug-1935: project_name and license_project are accepted job filters, so xcecli jobs list -P works. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6114f16dac
commit
4a38bb3ca9
18 changed files with 750 additions and 79 deletions
77
src/frontend/apiserver/controllers/job_scope.go
Normal file
77
src/frontend/apiserver/controllers/job_scope.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"apiserver/models"
|
||||
"apiserver/store"
|
||||
"apiserver/utils"
|
||||
)
|
||||
|
||||
// jobScope is what all of a user's grants of one job permission cover
|
||||
// (bug-1934): every job (scope all), or the jobs of a set of owners (scope
|
||||
// self: the user; scope self_ug: the user and everyone sharing a user group
|
||||
// with them). Grants add up; a scope type the job object does not support
|
||||
// covers nothing.
|
||||
type jobScope struct {
|
||||
all bool
|
||||
owners store.UserSet
|
||||
}
|
||||
|
||||
// userJobScope collects every grant of perm the user holds. It reads the
|
||||
// grants before DeduplicatePermissions, which keeps one scope per
|
||||
// permission and would drop the others.
|
||||
func userJobScope(user, perm string) (jobScope, error) {
|
||||
s := jobScope{owners: store.NewUserSet()}
|
||||
found := false
|
||||
for _, g := range collectUserPermissionsWithType(user) {
|
||||
if g.Permission != perm {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
switch strings.ToLower(g.DomainType) {
|
||||
case models.DomainTypeAll:
|
||||
s.all = true
|
||||
case models.DomainTypeSelf:
|
||||
s.owners = store.UnionUserSets(s.owners, store.NewUserSet(user))
|
||||
case models.DomainTypeSelfUG:
|
||||
s.owners = store.UnionUserSets(s.owners, store.UserGroupPeers(user))
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return s, errors.New("no permission")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// covers reports whether the scope covers a job owned by owner.
|
||||
func (s jobScope) covers(owner string) bool {
|
||||
return s.all || s.owners.Has(owner)
|
||||
}
|
||||
|
||||
// ownerFilter returns the job-query condition that keeps only covered
|
||||
// owners, or ok == false when the scope covers no one.
|
||||
func (s jobScope) ownerFilter() (f utils.FilterCondition, needed, ok bool) {
|
||||
if s.all {
|
||||
return f, false, true
|
||||
}
|
||||
names := make([]string, 0, len(s.owners.Names))
|
||||
for n := range s.owners.Names {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
if s.owners.All {
|
||||
if len(names) == 0 {
|
||||
return f, false, true
|
||||
}
|
||||
return utils.FilterCondition{Field: "submit_user", Operator: "notin",
|
||||
Value: "(" + strings.Join(names, ",") + ")"}, true, true
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return f, false, false
|
||||
}
|
||||
return utils.FilterCondition{Field: "submit_user", Operator: "in",
|
||||
Value: "(" + strings.Join(names, ",") + ")"}, true, true
|
||||
}
|
||||
179
src/frontend/apiserver/controllers/job_scope_test.go
Normal file
179
src/frontend/apiserver/controllers/job_scope_test.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"apiserver/models"
|
||||
"apiserver/store"
|
||||
)
|
||||
|
||||
// jobScopeTestRBAC: alice's job permissions reach only her user groups'
|
||||
// jobs (self_ug); dave holds self and self_ug together (bug-1934).
|
||||
const jobScopeTestRBAC = `
|
||||
permissions:
|
||||
- job:view
|
||||
- job:control
|
||||
- queue:submit
|
||||
- queue:control
|
||||
- queue:view
|
||||
roles:
|
||||
group_member:
|
||||
permissions:
|
||||
- action: job:view
|
||||
domain:
|
||||
- type: self_ug
|
||||
- action: job:control
|
||||
domain:
|
||||
- type: self_ug
|
||||
- action: queue:control
|
||||
domain:
|
||||
- type: all
|
||||
- action: queue:view
|
||||
domain:
|
||||
- type: all
|
||||
self_and_group:
|
||||
permissions:
|
||||
- action: job:view
|
||||
domain:
|
||||
- type: self
|
||||
- type: self_ug
|
||||
- action: job:control
|
||||
domain:
|
||||
- type: self
|
||||
- type: self_ug
|
||||
- action: queue:control
|
||||
domain:
|
||||
- type: all
|
||||
- action: queue:view
|
||||
domain:
|
||||
- type: all
|
||||
users:
|
||||
alice:
|
||||
roles:
|
||||
- name: group_member
|
||||
domain:
|
||||
- type: all
|
||||
dave:
|
||||
roles:
|
||||
- name: self_and_group
|
||||
domain:
|
||||
- type: all
|
||||
`
|
||||
|
||||
// jobScopeJobs are one RUN job per owner; alice and bob share user group
|
||||
// "team", carol and dave are in no group with alice, dave and erin share
|
||||
// "pair".
|
||||
type jobScopeJobs struct{ alice, bob, carol, dave, erin uint64 }
|
||||
|
||||
func seedJobScope(f *handlerFixture) jobScopeJobs {
|
||||
f.t.Helper()
|
||||
store.SetUserGroups([]models.UserGroupInfo{
|
||||
{Name: "team", Members: []string{"alice", "bob"}},
|
||||
{Name: "pair", Members: []string{"dave", "erin"}},
|
||||
})
|
||||
base := uint64(3)<<40 | uint64(1)<<20
|
||||
j := jobScopeJobs{alice: base + 1, bob: base + 2, carol: base + 3, dave: base + 4, erin: base + 5}
|
||||
for id, owner := range map[uint64]string{j.alice: "alice", j.bob: "bob",
|
||||
j.carol: "carol", j.dave: "dave", j.erin: "erin"} {
|
||||
f.seedJob(models.JobInfo{Unique_JobID: int64(id), JobID: int64(id),
|
||||
JobName: owner + "_job", JobStatus: models.JOBSTAT["RUN"], SubmitUser: owner})
|
||||
}
|
||||
return j
|
||||
}
|
||||
|
||||
// A self_ug grant lists the jobs of the requester's user groups and no
|
||||
// one else's, whatever submit_user the request names.
|
||||
func TestJobScope_SelfUGListCoversGroupOnly(t *testing.T) {
|
||||
f := newHandlerFixtureAs(t, jobScopeTestRBAC, "alice", "group_member")
|
||||
j := seedJobScope(f)
|
||||
|
||||
assertIDs(t, "submit_user all", rowIDs(t, f.listJobs("{submit_user:eq:all}")),
|
||||
[]uint64{j.alice, j.bob})
|
||||
assertIDs(t, "submit_user bob (group mate)", rowIDs(t, f.listJobs("{submit_user:eq:bob}")),
|
||||
[]uint64{j.bob})
|
||||
assertIDs(t, "submit_user carol (outside the group)",
|
||||
rowIDs(t, f.listJobs("{submit_user:eq:carol}")), nil)
|
||||
assertIDs(t, "no submit_user: own jobs", rowIDs(t, f.listJobs("{job_status:eq:RUN}")),
|
||||
[]uint64{j.alice})
|
||||
}
|
||||
|
||||
// GET /jobs/{ids} returns only covered jobs.
|
||||
func TestJobScope_SelfUGGetByID(t *testing.T) {
|
||||
f := newHandlerFixtureAs(t, jobScopeTestRBAC, "alice", "group_member")
|
||||
j := seedJobScope(f)
|
||||
|
||||
if w := f.getJobs(jobIDString(j.carol)); w.Code != http.StatusForbidden {
|
||||
t.Errorf("carol's job: status = %d, want 403; body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
assertIDs(t, "bob's job", rowIDs(t, f.getJobs(jobIDString(j.bob))), []uint64{j.bob})
|
||||
assertIDs(t, "bob's and carol's jobs",
|
||||
rowIDs(t, f.getJobs(jobIDString(j.bob)+","+jobIDString(j.carol))), []uint64{j.bob})
|
||||
}
|
||||
|
||||
// Job output follows the same scope: no group, no output.
|
||||
func TestJobScope_SelfUGOutput(t *testing.T) {
|
||||
f := newHandlerFixtureAs(t, jobScopeTestRBAC, "alice", "group_member")
|
||||
j := seedJobScope(f)
|
||||
|
||||
if w := f.do(http.MethodGet, "/xce/v1/jobs/"+jobIDString(j.carol)+"/output", nil); w.Code != http.StatusForbidden {
|
||||
t.Errorf("carol's output: status = %d, want 403; body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if w := f.do(http.MethodGet, "/xce/v1/jobs/"+jobIDString(j.bob)+"/output", nil); w.Code == http.StatusForbidden {
|
||||
t.Errorf("bob's output: status = 403, want access; body: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// job:control with self_ug controls group mates' jobs only.
|
||||
func TestJobScope_SelfUGControl(t *testing.T) {
|
||||
f := newHandlerFixtureAs(t, jobScopeTestRBAC, "alice", "group_member")
|
||||
j := seedJobScope(f)
|
||||
|
||||
if w := f.controlByID(jobIDString(j.carol), "kill"); w.Code != http.StatusForbidden {
|
||||
t.Errorf("kill carol's job: status = %d, want 403; body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if got := f.dispatchedIDs(); len(got) != 0 {
|
||||
t.Fatalf("carol's job was dispatched: %v", got)
|
||||
}
|
||||
if w := f.controlByID(jobIDString(j.bob), "kill"); w.Code != http.StatusOK {
|
||||
t.Fatalf("kill bob's job: status = %d, want 200; body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if got := f.dispatchedIDs(); len(got) != 1 || got[0] != j.bob {
|
||||
t.Errorf("dispatched = %v, want [%d]", got, j.bob)
|
||||
}
|
||||
|
||||
if w := f.controlByFilter(models.JobControlRequest{Action: "kill",
|
||||
Filter: models.JobControlFilter{Users: "carol"}}); w.Code != http.StatusForbidden {
|
||||
t.Errorf("filter kill of carol's jobs: status = %d, want 403; body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if got := f.dispatchedIDs(); len(got) != 1 {
|
||||
t.Errorf("filter control dispatched carol's job: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Several scopes of one permission add up: self and self_ug together cover
|
||||
// the requester's own jobs and the group's.
|
||||
func TestJobScope_GrantsAddUp(t *testing.T) {
|
||||
f := newHandlerFixtureAs(t, jobScopeTestRBAC, "dave", "self_and_group")
|
||||
j := seedJobScope(f)
|
||||
|
||||
assertIDs(t, "submit_user all", rowIDs(t, f.listJobs("{submit_user:eq:all}")),
|
||||
[]uint64{j.dave, j.erin})
|
||||
assertIDs(t, "erin's job by id", rowIDs(t, f.getJobs(jobIDString(j.erin))), []uint64{j.erin})
|
||||
}
|
||||
|
||||
// bug-1935: the project and license-project filters xcecli sends are
|
||||
// accepted and select by the job's project.
|
||||
func TestJobHandler_ProjectFilters(t *testing.T) {
|
||||
f := newHandlerFixture(t)
|
||||
base := uint64(4)<<40 | uint64(1)<<20
|
||||
f.seedJob(models.JobInfo{Unique_JobID: int64(base + 1), JobID: int64(base + 1),
|
||||
JobStatus: models.JOBSTAT["RUN"], ProjectName: "eng", LicenseProject: "lic1"})
|
||||
f.seedJob(models.JobInfo{Unique_JobID: int64(base + 2), JobID: int64(base + 2),
|
||||
JobStatus: models.JOBSTAT["RUN"], ProjectName: "ops", LicenseProject: "lic2"})
|
||||
|
||||
assertIDs(t, "project_name eng", rowIDs(t, f.listJobs("{project_name:eq:eng}")),
|
||||
[]uint64{base + 1})
|
||||
assertIDs(t, "license_project lic2", rowIDs(t, f.listJobs("{license_project:eq:lic2}")),
|
||||
[]uint64{base + 2})
|
||||
}
|
||||
|
|
@ -157,13 +157,18 @@ func (j JobController) GetJobsInfo(ctx *gin.Context) {
|
|||
}
|
||||
logger.Debug("get job info accessQueues", zap.Any("accessQueues", accessQueues),
|
||||
zap.Any("permission", permission))
|
||||
// check job:view permission
|
||||
permission, err = GetRequireResourcePermission(submitUser, "job:view")
|
||||
logger.Debug("get job info for job:view permission:", zap.Any("permission", permission))
|
||||
// check job:view permission: every grant counts (bug-1934)
|
||||
scope, err := userJobScope(submitUser, "job:view")
|
||||
logger.Debug("get job info for job:view scope:", zap.Any("scope", scope))
|
||||
if err != nil || len(accessQueues) == 0 {
|
||||
ReturnError(ctx, common.ERROR_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
ownerFilter, needOwnerFilter, anyOwner := scope.ownerFilter()
|
||||
if !anyOwner {
|
||||
ReturnError(ctx, common.ERROR_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// if submit_user is not specified, use query request submission user
|
||||
var specify_user bool = false
|
||||
|
|
@ -175,16 +180,17 @@ func (j JobController) GetJobsInfo(ctx *gin.Context) {
|
|||
if filter.Value == models.DomainTypeAll {
|
||||
delindex = i
|
||||
}
|
||||
if permission.DomainType != models.DomainTypeSelf {
|
||||
submitUser = filter.Value
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if delindex != -1 {
|
||||
filters = append(filters[:delindex], filters[delindex+1:]...)
|
||||
}
|
||||
if !specify_user || permission.DomainType == models.DomainTypeSelf {
|
||||
// a narrower scope keeps only the jobs of the owners it covers
|
||||
if needOwnerFilter {
|
||||
filters = append(filters, ownerFilter)
|
||||
}
|
||||
if !specify_user {
|
||||
logger.Debug("submit_user is not specified in query filter, add submit_user into filter: filter=[{submit_user:eq:" + submitUser)
|
||||
filters = append(filters, utils.FilterCondition{
|
||||
Field: "submit_user",
|
||||
|
|
@ -743,27 +749,29 @@ func (j JobController) GetJobByIDs(ctx *gin.Context) {
|
|||
} // end for jobid
|
||||
logger.Debug("the specific jobid query reply after merge:", zap.Any("jqueryReply", mergeJqueryReply))
|
||||
|
||||
// check rbac
|
||||
// check rbac: every job:view grant counts (bug-1934); rows are kept in
|
||||
// a new slice, since removing them from the one being ranged over
|
||||
// skipped the row after each removal
|
||||
removeFlag := false
|
||||
for i, row := range mergeJqueryReply {
|
||||
jobPermission, err := GetRequireResourcePermission(logonUser, "job:view")
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
queuePermission, err := GetRequireResourcePermission(logonUser, "queue:view")
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
jobScope, err := userJobScope(logonUser, "job:view")
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
queuePermission, err := GetRequireResourcePermission(logonUser, "queue:view")
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
visible := mergeJqueryReply[:0:0]
|
||||
for _, row := range mergeJqueryReply {
|
||||
if job_owner, exist := row.Data["submit_user"]; exist {
|
||||
if jobPermission.DomainType == models.DomainTypeSelf {
|
||||
if logonUser != job_owner.(string) {
|
||||
removeFlag = true
|
||||
mergeJqueryReply = append(mergeJqueryReply[:i], mergeJqueryReply[i+1:]...)
|
||||
}
|
||||
owner, _ := job_owner.(string)
|
||||
if !jobScope.covers(owner) {
|
||||
removeFlag = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
if typeValue != "array_stats" {
|
||||
|
|
@ -773,15 +781,16 @@ func (j JobController) GetJobByIDs(ctx *gin.Context) {
|
|||
permissionQueueValues := queuePermission.DomainValues
|
||||
if !utils.ContainsField(permissionQueueValues, queueName) {
|
||||
removeFlag = true
|
||||
mergeJqueryReply = append(mergeJqueryReply[:i], mergeJqueryReply[i+1:]...)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible = append(visible, row)
|
||||
}
|
||||
mergeJqueryReply = visible
|
||||
if removeFlag && len(mergeJqueryReply) == 0 {
|
||||
ReturnError(ctx, common.ERROR_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -2181,18 +2190,17 @@ func (j JobController) ControlJobByID(ctx *gin.Context) {
|
|||
|
||||
// check rbac
|
||||
if job_owner, exist := jobInfo.Data["submit_user"]; exist {
|
||||
jobPermission, err := GetRequireResourcePermission(submitter, "job:control")
|
||||
jobScope, err := userJobScope(submitter, "job:control")
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
if jobPermission.DomainType == models.DomainTypeSelf {
|
||||
if job_owner.(string) != submitter {
|
||||
logger.Error("Permission denied", zap.String("perm", "job:control"), zap.String("logonUser", submitter))
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
// bug-1934: every job:control grant counts
|
||||
if owner, _ := job_owner.(string); !jobScope.covers(owner) {
|
||||
logger.Error("Permission denied", zap.String("perm", "job:control"), zap.String("logonUser", submitter))
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2448,18 +2456,17 @@ func (j JobController) ControlJob(ctx *gin.Context) {
|
|||
|
||||
// check rbac begin
|
||||
if job_owner, exist := job.Data["submit_user"]; exist {
|
||||
jobPermission, err := GetRequireResourcePermission(submitter, "job:control")
|
||||
jobScope, err := userJobScope(submitter, "job:control")
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
if jobPermission.DomainType == models.DomainTypeSelf {
|
||||
if job_owner.(string) != submitter {
|
||||
logger.Error("Permission denied", zap.String("perm", "job:control"), zap.String("logonUser", submitter))
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
// bug-1934: every job:control grant counts
|
||||
if owner, _ := job_owner.(string); !jobScope.covers(owner) {
|
||||
logger.Error("Permission denied", zap.String("perm", "job:control"), zap.String("logonUser", submitter))
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,6 +125,13 @@ type handlerFixture struct {
|
|||
// restored in t.Cleanup.
|
||||
func newHandlerFixture(t *testing.T) *handlerFixture {
|
||||
t.Helper()
|
||||
return newHandlerFixtureAs(t, handlerTestRBAC, handlerTestUser, "handler_tester")
|
||||
}
|
||||
|
||||
// newHandlerFixtureAs is newHandlerFixture with the RBAC config and the
|
||||
// signed user and role given (bug-1934: tests of narrower job scopes).
|
||||
func newHandlerFixtureAs(t *testing.T, rbac, user, role string) *handlerFixture {
|
||||
t.Helper()
|
||||
|
||||
// Save every global this fixture (or setupRBACTest) replaces.
|
||||
origJobs, origArrayJobs, origQueues, origHosts := store.Jobs, store.ArrayJobs, store.Queues, store.Hosts
|
||||
|
|
@ -134,6 +141,7 @@ func newHandlerFixture(t *testing.T) *handlerFixture {
|
|||
origSubmit, origControl := submitJob, controlJob
|
||||
origJobCfg, origLog, origAuthCfg := utils.JobYamlConfig, utils.Log, utils.AuthenticationYamlConfig
|
||||
origRBAC := rbacConfig
|
||||
origUserGroups := store.UserGroupsSnapshot()
|
||||
|
||||
t.Cleanup(func() {
|
||||
store.Jobs, store.ArrayJobs, store.Queues, store.Hosts = origJobs, origArrayJobs, origQueues, origHosts
|
||||
|
|
@ -143,15 +151,16 @@ func newHandlerFixture(t *testing.T) *handlerFixture {
|
|||
submitJob, controlJob = origSubmit, origControl
|
||||
utils.JobYamlConfig, utils.Log, utils.AuthenticationYamlConfig = origJobCfg, origLog, origAuthCfg
|
||||
rbacConfig = origRBAC
|
||||
store.SetUserGroups(origUserGroups)
|
||||
})
|
||||
|
||||
// Auth/RBAC (replaces the stores with empty ones), then config.
|
||||
setupRBACTest(t, handlerTestRBAC)
|
||||
setupRBACTest(t, rbac)
|
||||
utils.JobYamlConfig = &utils.JobConfig{MaxArraySize: 1000}
|
||||
|
||||
f := &handlerFixture{
|
||||
t: t,
|
||||
token: signTestJWT(handlerTestUser, "handler_tester", time.Now().Add(time.Hour)),
|
||||
token: signTestJWT(user, role, time.Now().Add(time.Hour)),
|
||||
nextID: handlerTestOrdinaryBase,
|
||||
nextArray: handlerTestArrayBase,
|
||||
}
|
||||
|
|
@ -202,6 +211,7 @@ func newHandlerFixture(t *testing.T) *handlerFixture {
|
|||
g := r.Group("/xce/v1/jobs")
|
||||
g.GET("", jc.GetJobsInfo)
|
||||
g.GET("/:jobids", jc.GetJobByIDs)
|
||||
g.GET("/:jobids/output", jc.GetJobOutput)
|
||||
g.POST("/create", jc.CreateJob)
|
||||
g.POST("/:jobids/control", jc.ControlJobByID)
|
||||
g.POST("/:jobids/restart", jc.RestartJobByID)
|
||||
|
|
|
|||
|
|
@ -88,10 +88,11 @@ func (j JobController) GetJobOutput(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Check ownership: users with domain_type=all can view any job
|
||||
permission, perr := GetRequireResourcePermission(submitUser, "job:view")
|
||||
if perr != nil || permission.DomainType != models.DomainTypeAll {
|
||||
if job.SubmitUser != submitUser {
|
||||
// Check ownership: the owner may always read; anyone else needs a
|
||||
// job:view grant covering the job (bug-1934)
|
||||
if job.SubmitUser != submitUser {
|
||||
scope, perr := userJobScope(submitUser, "job:view")
|
||||
if perr != nil || !scope.covers(job.SubmitUser) {
|
||||
ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,6 +168,25 @@ func GetBuiltinRolePermissions(roleName string) []Permission {
|
|||
// get all user permission,
|
||||
// if user not exist in store, use basic_user permission
|
||||
func GetUserAllPermissionsWithType(userName string) []models.RolePermission {
|
||||
perms, fromRoles := collectUserPermissions(userName)
|
||||
if !fromRoles {
|
||||
return perms
|
||||
}
|
||||
permissionListsType := DeduplicatePermissions(perms)
|
||||
logger.Debug("case 4 DeduplicatePermissions", zap.Any("permissionListsType", permissionListsType))
|
||||
return permissionListsType
|
||||
}
|
||||
|
||||
// collectUserPermissionsWithType returns every grant of the user, one entry
|
||||
// per scope, before DeduplicatePermissions (bug-1934: job scopes add up).
|
||||
func collectUserPermissionsWithType(userName string) []models.RolePermission {
|
||||
perms, _ := collectUserPermissions(userName)
|
||||
return perms
|
||||
}
|
||||
|
||||
// collectUserPermissions returns the user's grants; fromRoles is false when
|
||||
// the user has no roles and gets the builtin basic_user grants.
|
||||
func collectUserPermissions(userName string) ([]models.RolePermission, bool) {
|
||||
userRoles := store.GetUserRoleNames(userName)
|
||||
if len(userRoles) == 0 {
|
||||
// case 1: user not in store, use basic_user
|
||||
|
|
@ -184,7 +203,7 @@ func GetUserAllPermissionsWithType(userName string) []models.RolePermission {
|
|||
}
|
||||
}
|
||||
logger.Debug("case 1, assign basic_user permission to unregistered user")
|
||||
return permissionTransform
|
||||
return permissionTransform, false
|
||||
}
|
||||
|
||||
builtInRoles := utils.IncludeBuiltinRoles(userRoles)
|
||||
|
|
@ -212,9 +231,7 @@ func GetUserAllPermissionsWithType(userName string) []models.RolePermission {
|
|||
permissionListsType = append(permissionListsType, GetRoleAllPermissionsWithType(userRole)...)
|
||||
}
|
||||
logger.Debug("case 3, before remove duplicated permissions", zap.Any("permissionListsType", permissionListsType))
|
||||
permissionListsType = DeduplicatePermissions(permissionListsType)
|
||||
logger.Debug("case 4 DeduplicatePermissions", zap.Any("permissionListsType", permissionListsType))
|
||||
return permissionListsType
|
||||
return permissionListsType, true
|
||||
}
|
||||
|
||||
func GetRoleAllPermissionsWithType(roleName string) []models.RolePermission {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue