Fix process and iam enforcing credentials

This commit is contained in:
Ingo Oppermann
2025-07-07 13:14:00 +02:00
parent 15a0f4dbc5
commit 290c612d01
4 changed files with 82 additions and 103 deletions

View File

@@ -122,6 +122,7 @@ func (r *ProcessReport) Marshal() app.Report {
type ProcessReportSearchResult struct { type ProcessReportSearchResult struct {
ProcessID string `json:"id"` ProcessID string `json:"id"`
Domain string `json:"domain"`
Reference string `json:"reference"` Reference string `json:"reference"`
ExitState string `json:"exit_state"` ExitState string `json:"exit_state"`
CreatedAt int64 `json:"created_at" format:"int64"` CreatedAt int64 `json:"created_at" format:"int64"`

View File

@@ -22,7 +22,6 @@ import (
// @Tags v16.?.? // @Tags v16.?.?
// @ID cluster-3-get-all-processes // @ID cluster-3-get-all-processes
// @Produce json // @Produce json
// @Param domain query string false "Domain to act on"
// @Param filter query string false "Comma separated list of fields (config, state, report, metadata) that will be part of the output. If empty, all fields will be part of the output." // @Param filter query string false "Comma separated list of fields (config, state, report, metadata) that will be part of the output. If empty, all fields will be part of the output."
// @Param reference query string false "Return only these process that have this reference value. If empty, the reference will be ignored." // @Param reference query string false "Return only these process that have this reference value. If empty, the reference will be ignored."
// @Param id query string false "Comma separated list of process ids to list. Overrides the reference. If empty all IDs will be returned." // @Param id query string false "Comma separated list of process ids to list. Overrides the reference. If empty all IDs will be returned."
@@ -40,16 +39,15 @@ func (h *ClusterHandler) ProcessList(c echo.Context) error {
wantids := strings.FieldsFunc(util.DefaultQuery(c, "id", ""), func(r rune) bool { wantids := strings.FieldsFunc(util.DefaultQuery(c, "id", ""), func(r rune) bool {
return r == rune(',') return r == rune(',')
}) })
domain := util.DefaultQuery(c, "domain", "")
idpattern := util.DefaultQuery(c, "idpattern", "") idpattern := util.DefaultQuery(c, "idpattern", "")
refpattern := util.DefaultQuery(c, "refpattern", "") refpattern := util.DefaultQuery(c, "refpattern", "")
ownerpattern := util.DefaultQuery(c, "ownerpattern", "") ownerpattern := util.DefaultQuery(c, "ownerpattern", "")
domainpattern := util.DefaultQuery(c, "domainpattern", "") domainpattern := util.DefaultQuery(c, "domainpattern", "")
procs := h.proxy.ProcessList(node.ProcessListOptions{ procs := h.proxy.ProcessList(node.ProcessListOptions{
ID: wantids, ID: wantids,
Filter: filter.Slice(), Filter: filter.Slice(),
Domain: domain, //Domain: domain,
Reference: reference, Reference: reference,
IDPattern: idpattern, IDPattern: idpattern,
RefPattern: refpattern, RefPattern: refpattern,
@@ -60,7 +58,7 @@ func (h *ClusterHandler) ProcessList(c echo.Context) error {
pmap := map[app.ProcessID]api.Process{} pmap := map[app.ProcessID]api.Process{}
for _, p := range procs { for _, p := range procs {
if !h.iam.Enforce(ctxuser, domain, "process", p.ID, "read") { if !h.iam.Enforce(ctxuser, p.Domain, "process", p.ID, "read") {
continue continue
} }
@@ -72,10 +70,10 @@ func (h *ClusterHandler) ProcessList(c echo.Context) error {
// Here we have to add those processes that are in the cluster DB and couldn't be deployed // Here we have to add those processes that are in the cluster DB and couldn't be deployed
{ {
processes := h.cluster.Store().ProcessList() processes := h.cluster.Store().ProcessList()
filtered := h.getFilteredStoreProcesses(processes, wantids, domain, reference, idpattern, refpattern, ownerpattern, domainpattern) filtered := h.getFilteredStoreProcesses(processes, wantids, reference, idpattern, refpattern, ownerpattern, domainpattern)
for _, p := range filtered { for _, p := range filtered {
if !h.iam.Enforce(ctxuser, domain, "process", p.Config.ID, "read") { if !h.iam.Enforce(ctxuser, p.Config.Domain, "process", p.Config.ID, "read") {
continue continue
} }
@@ -105,7 +103,7 @@ func (h *ClusterHandler) ProcessList(c echo.Context) error {
return c.JSON(http.StatusOK, processes) return c.JSON(http.StatusOK, processes)
} }
func (h *ClusterHandler) getFilteredStoreProcesses(processes []store.Process, wantids []string, _, reference, idpattern, refpattern, ownerpattern, domainpattern string) []store.Process { func (h *ClusterHandler) getFilteredStoreProcesses(processes []store.Process, wantids []string, reference, idpattern, refpattern, ownerpattern, domainpattern string) []store.Process {
filtered := []store.Process{} filtered := []store.Process{}
count := 0 count := 0
@@ -252,7 +250,6 @@ func (h *ClusterHandler) ProcessGet(c echo.Context) error {
// @Router /api/v3/cluster/process [post] // @Router /api/v3/cluster/process [post]
func (h *ClusterHandler) ProcessAdd(c echo.Context) error { func (h *ClusterHandler) ProcessAdd(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "") ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
process := api.ProcessConfig{ process := api.ProcessConfig{
ID: shortuuid.New(), ID: shortuuid.New(),
@@ -269,12 +266,6 @@ func (h *ClusterHandler) ProcessAdd(c echo.Context) error {
return api.Err(http.StatusForbidden, "", "API user %s is not allowed to write this process in domain %s", ctxuser, process.Domain) return api.Err(http.StatusForbidden, "", "API user %s is not allowed to write this process in domain %s", ctxuser, process.Domain)
} }
if !superuser {
if !h.iam.Enforce(process.Owner, process.Domain, "process", process.ID, "write") {
return api.Err(http.StatusForbidden, "", "user %s is not allowed to write this process in domain %s", process.Owner, process.Domain)
}
}
if process.Type != "ffmpeg" { if process.Type != "ffmpeg" {
return api.Err(http.StatusBadRequest, "", "unsupported process type: supported process types are: ffmpeg") return api.Err(http.StatusBadRequest, "", "unsupported process type: supported process types are: ffmpeg")
} }
@@ -314,7 +305,6 @@ func (h *ClusterHandler) ProcessAdd(c echo.Context) error {
// @Router /api/v3/cluster/process/{id} [put] // @Router /api/v3/cluster/process/{id} [put]
func (h *ClusterHandler) ProcessUpdate(c echo.Context) error { func (h *ClusterHandler) ProcessUpdate(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "") ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
domain := util.DefaultQuery(c, "domain", "") domain := util.DefaultQuery(c, "domain", "")
id := util.PathParam(c, "id") id := util.PathParam(c, "id")
@@ -348,12 +338,6 @@ func (h *ClusterHandler) ProcessUpdate(c echo.Context) error {
return api.Err(http.StatusForbidden, "", "API user %s is not allowed to write this process", ctxuser) return api.Err(http.StatusForbidden, "", "API user %s is not allowed to write this process", ctxuser)
} }
if !superuser {
if !h.iam.Enforce(process.Owner, process.Domain, "process", process.ID, "write") {
return api.Err(http.StatusForbidden, "", "user %s is not allowed to write this process", process.Owner)
}
}
config, metadata := process.Marshal() config, metadata := process.Marshal()
if err := h.cluster.ProcessUpdate("", pid, config); err != nil { if err := h.cluster.ProcessUpdate("", pid, config); err != nil {

View File

@@ -292,13 +292,13 @@ func (h *IAMHandler) UpdateIdentityPolicies(c echo.Context) error {
// @Router /api/v3/iam/user [get] // @Router /api/v3/iam/user [get]
func (h *IAMHandler) ListIdentities(c echo.Context) error { func (h *IAMHandler) ListIdentities(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "") ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "$none") domain := util.DefaultQuery(c, "domain", "")
identities := h.iam.ListIdentities() identities := h.iam.ListIdentities()
users := make([]api.IAMUser, len(identities)+1) users := []api.IAMUser{}
for i, iamuser := range identities { for _, iamuser := range identities {
if !h.iam.Enforce(ctxuser, domain, "iam", iamuser.Name, "read") { if !h.iam.Enforce(ctxuser, domain, "iam", iamuser.Name, "read") {
continue continue
} }
@@ -311,7 +311,9 @@ func (h *IAMHandler) ListIdentities(c echo.Context) error {
policies := h.iam.ListPolicies(iamuser.Name, "", nil, "", nil) policies := h.iam.ListPolicies(iamuser.Name, "", nil, "", nil)
users[i].Marshal(iamuser, policies) user := api.IAMUser{}
user.Marshal(iamuser, policies)
users = append(users, user)
} }
anon := identity.User{ anon := identity.User{
@@ -320,7 +322,9 @@ func (h *IAMHandler) ListIdentities(c echo.Context) error {
policies := h.iam.ListPolicies("$anon", "", nil, "", nil) policies := h.iam.ListPolicies("$anon", "", nil, "", nil)
users[len(users)-1].Marshal(anon, policies) user := api.IAMUser{}
user.Marshal(anon, policies)
users = append(users, user)
return c.JSON(http.StatusOK, users) return c.JSON(http.StatusOK, users)
} }

View File

@@ -50,7 +50,6 @@ func NewProcess(restream restream.Restreamer, iam iam.IAM) *ProcessHandler {
// @Router /api/v3/process [post] // @Router /api/v3/process [post]
func (h *ProcessHandler) Add(c echo.Context) error { func (h *ProcessHandler) Add(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "") ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
process := api.ProcessConfig{ process := api.ProcessConfig{
ID: shortuuid.New(), ID: shortuuid.New(),
@@ -67,12 +66,6 @@ func (h *ProcessHandler) Add(c echo.Context) error {
return api.Err(http.StatusForbidden, "", "You are not allowed to write this process, check the domain and process ID") return api.Err(http.StatusForbidden, "", "You are not allowed to write this process, check the domain and process ID")
} }
if !superuser {
if !h.iam.Enforce(process.Owner, process.Domain, "process", process.ID, "write") {
return api.Err(http.StatusForbidden, "", "The owner '%s' is not allowed to write this process", process.Owner)
}
}
if process.Type != "ffmpeg" { if process.Type != "ffmpeg" {
return api.Err(http.StatusBadRequest, "", "unsupported process type, supported process types are: ffmpeg") return api.Err(http.StatusBadRequest, "", "unsupported process type, supported process types are: ffmpeg")
} }
@@ -107,7 +100,6 @@ func (h *ProcessHandler) Add(c echo.Context) error {
// @Tags v16.7.2 // @Tags v16.7.2
// @ID process-3-get-all // @ID process-3-get-all
// @Produce json // @Produce json
// @Param domain query string false "Domain to act on"
// @Param filter query string false "Comma separated list of fields (config, state, report, metadata) that will be part of the output. If empty, all fields will be part of the output." // @Param filter query string false "Comma separated list of fields (config, state, report, metadata) that will be part of the output. If empty, all fields will be part of the output."
// @Param reference query string false "Return only these process that have this reference value. If empty, the reference will be ignored." // @Param reference query string false "Return only these process that have this reference value. If empty, the reference will be ignored."
// @Param id query string false "Comma separated list of process ids to list. Overrides the reference. If empty all IDs will be returned." // @Param id query string false "Comma separated list of process ids to list. Overrides the reference. If empty all IDs will be returned."
@@ -125,7 +117,6 @@ func (h *ProcessHandler) GetAll(c echo.Context) error {
wantids := strings.FieldsFunc(util.DefaultQuery(c, "id", ""), func(r rune) bool { wantids := strings.FieldsFunc(util.DefaultQuery(c, "id", ""), func(r rune) bool {
return r == rune(',') return r == rune(',')
}) })
domain := util.DefaultQuery(c, "domain", "")
idpattern := util.DefaultQuery(c, "idpattern", "") idpattern := util.DefaultQuery(c, "idpattern", "")
refpattern := util.DefaultQuery(c, "refpattern", "") refpattern := util.DefaultQuery(c, "refpattern", "")
ownerpattern := util.DefaultQuery(c, "ownerpattern", "") ownerpattern := util.DefaultQuery(c, "ownerpattern", "")
@@ -157,19 +148,19 @@ func (h *ProcessHandler) GetAll(c echo.Context) error {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
for i := 0; i < 8; /*runtime.NumCPU()*/ i++ { for range 8 {
wg.Add(1) wg.Add(1)
go func(idChan <-chan app.ProcessID) { go func(idChan <-chan app.ProcessID) {
defer wg.Done() defer wg.Done()
for id := range idChan { for id := range idChan {
if !h.iam.Enforce(ctxuser, domain, "process", id.ID, "read") { process, err := h.getProcess(id, filter)
if err != nil {
continue continue
} }
process, err := h.getProcess(id, filter) if !h.iam.Enforce(ctxuser, process.Domain, "process", id.ID, "read") {
if err != nil {
continue continue
} }
@@ -218,7 +209,7 @@ func (h *ProcessHandler) Get(c echo.Context) error {
domain := util.DefaultQuery(c, "domain", "") domain := util.DefaultQuery(c, "domain", "")
if !h.iam.Enforce(ctxuser, domain, "process", id, "read") { if !h.iam.Enforce(ctxuser, domain, "process", id, "read") {
return api.Err(http.StatusForbidden, "Forbidden") return api.Err(http.StatusForbidden, "")
} }
tid := app.ProcessID{ tid := app.ProcessID{
@@ -251,7 +242,6 @@ func (h *ProcessHandler) Get(c echo.Context) error {
// @Router /api/v3/process/{id} [delete] // @Router /api/v3/process/{id} [delete]
func (h *ProcessHandler) Delete(c echo.Context) error { func (h *ProcessHandler) Delete(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "") ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
id := util.PathParam(c, "id") id := util.PathParam(c, "id")
domain := util.DefaultQuery(c, "domain", "") domain := util.DefaultQuery(c, "domain", "")
@@ -260,10 +250,8 @@ func (h *ProcessHandler) Delete(c echo.Context) error {
Domain: domain, Domain: domain,
} }
if !superuser { if !h.iam.Enforce(ctxuser, domain, "process", id, "write") {
if !h.iam.Enforce(ctxuser, domain, "process", id, "write") { return api.Err(http.StatusForbidden, "")
return api.Err(http.StatusForbidden, "")
}
} }
if err := h.restream.StopProcess(tid); err != nil { if err := h.restream.StopProcess(tid); err != nil {
@@ -296,7 +284,6 @@ func (h *ProcessHandler) Delete(c echo.Context) error {
// @Router /api/v3/process/{id} [put] // @Router /api/v3/process/{id} [put]
func (h *ProcessHandler) Update(c echo.Context) error { func (h *ProcessHandler) Update(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "") ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
domain := util.DefaultQuery(c, "domain", "") domain := util.DefaultQuery(c, "domain", "")
id := util.PathParam(c, "id") id := util.PathParam(c, "id")
@@ -333,12 +320,6 @@ func (h *ProcessHandler) Update(c echo.Context) error {
return api.Err(http.StatusForbidden, "", "You are not allowed to write this process: %s", process.ID) return api.Err(http.StatusForbidden, "", "You are not allowed to write this process: %s", process.ID)
} }
if !superuser {
if !h.iam.Enforce(process.Owner, process.Domain, "process", process.ID, "write") {
return api.Err(http.StatusForbidden, "", "The owner '%s' is not allowed to write this process: %s", process.Owner, process.ID)
}
}
config, metadata := process.Marshal() config, metadata := process.Marshal()
if err := h.restream.UpdateProcess(tid, config); err != nil { if err := h.restream.UpdateProcess(tid, config); err != nil {
@@ -397,15 +378,16 @@ func (h *ProcessHandler) Command(c echo.Context) error {
} }
var err error var err error
if command.Command == "start" { switch command.Command {
case "start":
err = h.restream.StartProcess(tid) err = h.restream.StartProcess(tid)
} else if command.Command == "stop" { case "stop":
err = h.restream.StopProcess(tid) err = h.restream.StopProcess(tid)
} else if command.Command == "restart" { case "restart":
err = h.restream.RestartProcess(tid) err = h.restream.RestartProcess(tid)
} else if command.Command == "reload" { case "reload":
err = h.restream.ReloadProcess(tid) err = h.restream.ReloadProcess(tid)
} else { default:
return api.Err(http.StatusBadRequest, "", "unknown command provided: known commands are: start, stop, reload, restart") return api.Err(http.StatusBadRequest, "", "unknown command provided: known commands are: start, stop, reload, restart")
} }
@@ -673,6 +655,7 @@ func (h *ProcessHandler) SetReport(c echo.Context) error {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /api/v3/report/process [get] // @Router /api/v3/report/process [get]
func (h *ProcessHandler) SearchReportHistory(c echo.Context) error { func (h *ProcessHandler) SearchReportHistory(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
idpattern := util.DefaultQuery(c, "idpattern", "") idpattern := util.DefaultQuery(c, "idpattern", "")
refpattern := util.DefaultQuery(c, "refpattern", "") refpattern := util.DefaultQuery(c, "refpattern", "")
state := util.DefaultQuery(c, "state", "") state := util.DefaultQuery(c, "state", "")
@@ -701,13 +684,20 @@ func (h *ProcessHandler) SearchReportHistory(c echo.Context) error {
result := h.restream.SearchProcessLogHistory(idpattern, refpattern, state, from, to) result := h.restream.SearchProcessLogHistory(idpattern, refpattern, state, from, to)
response := make([]api.ProcessReportSearchResult, len(result)) response := []api.ProcessReportSearchResult{}
for i, b := range result { for _, b := range result {
response[i].ProcessID = b.ProcessID if !h.iam.Enforce(ctxuser, b.Domain, "process", b.ProcessID, "read") {
response[i].Reference = b.Reference continue
response[i].ExitState = b.ExitState }
response[i].CreatedAt = b.CreatedAt.Unix()
response[i].ExitedAt = b.ExitedAt.Unix() response = append(response, api.ProcessReportSearchResult{
ProcessID: b.ProcessID,
Domain: b.Domain,
Reference: b.Reference,
ExitState: b.ExitState,
CreatedAt: b.CreatedAt.Unix(),
ExitedAt: b.ExitedAt.Unix(),
})
} }
return c.JSON(http.StatusOK, response) return c.JSON(http.StatusOK, response)
@@ -843,43 +833,6 @@ func (h *ProcessHandler) ValidateConfig(c echo.Context) error {
return c.JSON(http.StatusOK, process) return c.JSON(http.StatusOK, process)
} }
// Skills returns the detected FFmpeg capabilities
// @Summary FFmpeg capabilities
// @Description List all detected FFmpeg capabilities.
// @Tags v16.7.2
// @ID skills-3
// @Produce json
// @Success 200 {object} api.Skills
// @Security ApiKeyAuth
// @Router /api/v3/skills [get]
func (h *ProcessHandler) Skills(c echo.Context) error {
skills := h.restream.Skills()
apiskills := api.Skills{}
apiskills.Unmarshal(skills)
return c.JSON(http.StatusOK, apiskills)
}
// ReloadSkills will refresh the FFmpeg capabilities
// @Summary Refresh FFmpeg capabilities
// @Description Refresh the available FFmpeg capabilities.
// @Tags v16.7.2
// @ID skills-3-reload
// @Produce json
// @Success 200 {object} api.Skills
// @Security ApiKeyAuth
// @Router /api/v3/skills/reload [get]
func (h *ProcessHandler) ReloadSkills(c echo.Context) error {
h.restream.ReloadSkills()
skills := h.restream.Skills()
apiskills := api.Skills{}
apiskills.Unmarshal(skills)
return c.JSON(http.StatusOK, apiskills)
}
// GetProcessMetadata returns the metadata stored with a process // GetProcessMetadata returns the metadata stored with a process
// @Summary Retrieve JSON metadata stored with a process under a key // @Summary Retrieve JSON metadata stored with a process under a key
// @Description Retrieve the previously stored JSON metadata under the given key. If the key is empty, all metadata will be returned. // @Description Retrieve the previously stored JSON metadata under the given key. If the key is empty, all metadata will be returned.
@@ -1028,6 +981,43 @@ func (h *ProcessHandler) SetMetadata(c echo.Context) error {
return c.JSON(http.StatusOK, data) return c.JSON(http.StatusOK, data)
} }
// Skills returns the detected FFmpeg capabilities
// @Summary FFmpeg capabilities
// @Description List all detected FFmpeg capabilities.
// @Tags v16.7.2
// @ID skills-3
// @Produce json
// @Success 200 {object} api.Skills
// @Security ApiKeyAuth
// @Router /api/v3/skills [get]
func (h *ProcessHandler) Skills(c echo.Context) error {
skills := h.restream.Skills()
apiskills := api.Skills{}
apiskills.Unmarshal(skills)
return c.JSON(http.StatusOK, apiskills)
}
// ReloadSkills will refresh the FFmpeg capabilities
// @Summary Refresh FFmpeg capabilities
// @Description Refresh the available FFmpeg capabilities.
// @Tags v16.7.2
// @ID skills-3-reload
// @Produce json
// @Success 200 {object} api.Skills
// @Security ApiKeyAuth
// @Router /api/v3/skills/reload [get]
func (h *ProcessHandler) ReloadSkills(c echo.Context) error {
h.restream.ReloadSkills()
skills := h.restream.Skills()
apiskills := api.Skills{}
apiskills.Unmarshal(skills)
return c.JSON(http.StatusOK, apiskills)
}
type filter struct { type filter struct {
config bool config bool
state bool state bool