Allow global admins to view deleted namespace logs
Signed-off-by: huanggze <loganhuang@yunify.com>
This commit is contained in:
@@ -27,8 +27,6 @@ type APIResponse struct {
|
|||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Operation string
|
Operation string
|
||||||
WorkspaceFilter string
|
|
||||||
WorkspaceSearch string
|
|
||||||
NamespaceFilter string
|
NamespaceFilter string
|
||||||
NamespaceSearch string
|
NamespaceSearch string
|
||||||
WorkloadFilter string
|
WorkloadFilter string
|
||||||
@@ -49,8 +47,6 @@ type Query struct {
|
|||||||
func ParseQueryParameter(req *restful.Request) (*Query, error) {
|
func ParseQueryParameter(req *restful.Request) (*Query, error) {
|
||||||
var q Query
|
var q Query
|
||||||
q.Operation = req.QueryParameter("operation")
|
q.Operation = req.QueryParameter("operation")
|
||||||
q.WorkspaceFilter = req.QueryParameter("workspaces")
|
|
||||||
q.WorkspaceSearch = req.QueryParameter("workspace_query")
|
|
||||||
q.NamespaceFilter = req.QueryParameter("namespaces")
|
q.NamespaceFilter = req.QueryParameter("namespaces")
|
||||||
q.NamespaceSearch = req.QueryParameter("namespace_query")
|
q.NamespaceSearch = req.QueryParameter("namespace_query")
|
||||||
q.WorkloadFilter = req.QueryParameter("workloads")
|
q.WorkloadFilter = req.QueryParameter("workloads")
|
||||||
|
|||||||
@@ -192,8 +192,6 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
|
|||||||
To(handler.QueryLogs).
|
To(handler.QueryLogs).
|
||||||
Doc("Query logs against the cluster.").
|
Doc("Query logs against the cluster.").
|
||||||
Param(ws.QueryParameter("operation", "Operation type. This can be one of four types: query (for querying logs), statistics (for retrieving statistical data), histogram (for displaying log count by time interval) and export (for exporting logs). Defaults to query.").DefaultValue("query").DataType("string").Required(false)).
|
Param(ws.QueryParameter("operation", "Operation type. This can be one of four types: query (for querying logs), statistics (for retrieving statistical data), histogram (for displaying log count by time interval) and export (for exporting logs). Defaults to query.").DefaultValue("query").DataType("string").Required(false)).
|
||||||
Param(ws.QueryParameter("workspaces", "A comma-separated list of workspaces. This field restricts the query to specified workspaces. For example, the following filter matches the workspace my-ws and demo-ws: `my-ws,demo-ws`").DataType("string").Required(false)).
|
|
||||||
Param(ws.QueryParameter("workspace_query", "A comma-separated list of keywords. Differing from **workspaces**, this field performs fuzzy matching on workspaces. For example, the following value limits the query to workspaces whose name contains the word my(My,MY,...) *OR* demo(Demo,DemO,...): `my,demo`.").DataType("string").Required(false)).
|
|
||||||
Param(ws.QueryParameter("namespaces", "A comma-separated list of namespaces. This field restricts the query to specified namespaces. For example, the following filter matches the namespace my-ns and demo-ns: `my-ns,demo-ns`").DataType("string").Required(false)).
|
Param(ws.QueryParameter("namespaces", "A comma-separated list of namespaces. This field restricts the query to specified namespaces. For example, the following filter matches the namespace my-ns and demo-ns: `my-ns,demo-ns`").DataType("string").Required(false)).
|
||||||
Param(ws.QueryParameter("namespace_query", "A comma-separated list of keywords. Differing from **namespaces**, this field performs fuzzy matching on namespaces. For example, the following value limits the query to namespaces whose name contains the word my(My,MY,...) *OR* demo(Demo,DemO,...): `my,demo`.").DataType("string").Required(false)).
|
Param(ws.QueryParameter("namespace_query", "A comma-separated list of keywords. Differing from **namespaces**, this field performs fuzzy matching on namespaces. For example, the following value limits the query to namespaces whose name contains the word my(My,MY,...) *OR* demo(Demo,DemO,...): `my,demo`.").DataType("string").Required(false)).
|
||||||
Param(ws.QueryParameter("workloads", "A comma-separated list of workloads. This field restricts the query to specified workloads. For example, the following filter matches the workload my-wl and demo-wl: `my-wl,demo-wl`").DataType("string").Required(false)).
|
Param(ws.QueryParameter("workloads", "A comma-separated list of workloads. This field restricts the query to specified workloads. For example, the following filter matches the workload my-wl and demo-wl: `my-wl,demo-wl`").DataType("string").Required(false)).
|
||||||
|
|||||||
@@ -563,6 +563,7 @@ func (t *tenantOperator) DeleteWorkspace(workspace string) error {
|
|||||||
// 2. If `workspaceSubstrs` is not empty, the namespace SHOULD belong to a workspace whose name contains one of the specified substrings.
|
// 2. If `workspaceSubstrs` is not empty, the namespace SHOULD belong to a workspace whose name contains one of the specified substrings.
|
||||||
// 3. If `namespaces` is not empty, the namespace SHOULD be one of the specified namespacs.
|
// 3. If `namespaces` is not empty, the namespace SHOULD be one of the specified namespacs.
|
||||||
// 4. If `namespaceSubstrs` is not empty, the namespace's name SHOULD contain one of the specified substrings.
|
// 4. If `namespaceSubstrs` is not empty, the namespace's name SHOULD contain one of the specified substrings.
|
||||||
|
// 5. If ALL of the filters above are empty, returns all namespaces.
|
||||||
func (t *tenantOperator) listIntersectedNamespaces(workspaces, workspaceSubstrs,
|
func (t *tenantOperator) listIntersectedNamespaces(workspaces, workspaceSubstrs,
|
||||||
namespaces, namespaceSubstrs []string) ([]*corev1.Namespace, error) {
|
namespaces, namespaceSubstrs []string) ([]*corev1.Namespace, error) {
|
||||||
var (
|
var (
|
||||||
@@ -670,9 +671,7 @@ func (t *tenantOperator) Events(user user.Info, queryParam *eventsv1alpha1.Query
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *tenantOperator) QueryLogs(user user.Info, query *loggingv1alpha2.Query) (*loggingv1alpha2.APIResponse, error) {
|
func (t *tenantOperator) QueryLogs(user user.Info, query *loggingv1alpha2.Query) (*loggingv1alpha2.APIResponse, error) {
|
||||||
iNamespaces, err := t.listIntersectedNamespaces(
|
iNamespaces, err := t.listIntersectedNamespaces(nil, nil,
|
||||||
stringutils.Split(query.WorkspaceFilter, ","),
|
|
||||||
stringutils.Split(query.WorkspaceSearch, ","),
|
|
||||||
stringutils.Split(query.NamespaceFilter, ","),
|
stringutils.Split(query.NamespaceFilter, ","),
|
||||||
stringutils.Split(query.NamespaceSearch, ","))
|
stringutils.Split(query.NamespaceSearch, ","))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -680,7 +679,37 @@ func (t *tenantOperator) QueryLogs(user user.Info, query *loggingv1alpha2.Query)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
namespaceCreateTimeMap := make(map[string]time.Time)
|
namespaceCreateTimeMap := make(map[string]*time.Time)
|
||||||
|
|
||||||
|
var isGlobalAdmin bool
|
||||||
|
|
||||||
|
// If it is a global admin, the user can view logs from any namespace.
|
||||||
|
podLogs := authorizer.AttributesRecord{
|
||||||
|
User: user,
|
||||||
|
Verb: "get",
|
||||||
|
APIGroup: "",
|
||||||
|
APIVersion: "v1",
|
||||||
|
Resource: "pods",
|
||||||
|
Subresource: "log",
|
||||||
|
ResourceRequest: true,
|
||||||
|
ResourceScope: request.ClusterScope,
|
||||||
|
}
|
||||||
|
decision, _, err := t.authorizer.Authorize(podLogs)
|
||||||
|
if err != nil {
|
||||||
|
klog.Error(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if decision == authorizer.DecisionAllow {
|
||||||
|
isGlobalAdmin = true
|
||||||
|
if query.NamespaceFilter != "" || query.NamespaceSearch != "" {
|
||||||
|
for _, ns := range iNamespaces {
|
||||||
|
namespaceCreateTimeMap[ns.Name] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is a regular user, this user can only view logs of namespaces the user belongs to.
|
||||||
|
if !isGlobalAdmin {
|
||||||
for _, ns := range iNamespaces {
|
for _, ns := range iNamespaces {
|
||||||
podLogs := authorizer.AttributesRecord{
|
podLogs := authorizer.AttributesRecord{
|
||||||
User: user,
|
User: user,
|
||||||
@@ -699,7 +728,8 @@ func (t *tenantOperator) QueryLogs(user user.Info, query *loggingv1alpha2.Query)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if decision == authorizer.DecisionAllow {
|
if decision == authorizer.DecisionAllow {
|
||||||
namespaceCreateTimeMap[ns.Name] = ns.CreationTimestamp.Time
|
namespaceCreateTimeMap[ns.Name] = &ns.CreationTimestamp.Time
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,21 +747,24 @@ func (t *tenantOperator) QueryLogs(user user.Info, query *loggingv1alpha2.Query)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ar loggingv1alpha2.APIResponse
|
var ar loggingv1alpha2.APIResponse
|
||||||
|
noHit := !isGlobalAdmin && len(namespaceCreateTimeMap) == 0 ||
|
||||||
|
isGlobalAdmin && len(namespaceCreateTimeMap) == 0 && (query.NamespaceFilter != "" || query.NamespaceSearch != "")
|
||||||
|
|
||||||
switch query.Operation {
|
switch query.Operation {
|
||||||
case loggingv1alpha2.OperationStatistics:
|
case loggingv1alpha2.OperationStatistics:
|
||||||
if len(namespaceCreateTimeMap) == 0 {
|
if noHit {
|
||||||
ar.Statistics = &loggingclient.Statistics{}
|
ar.Statistics = &loggingclient.Statistics{}
|
||||||
} else {
|
} else {
|
||||||
ar, err = t.lo.GetCurrentStats(sf)
|
ar, err = t.lo.GetCurrentStats(sf)
|
||||||
}
|
}
|
||||||
case loggingv1alpha2.OperationHistogram:
|
case loggingv1alpha2.OperationHistogram:
|
||||||
if len(namespaceCreateTimeMap) == 0 {
|
if noHit {
|
||||||
ar.Histogram = &loggingclient.Histogram{}
|
ar.Histogram = &loggingclient.Histogram{}
|
||||||
} else {
|
} else {
|
||||||
ar, err = t.lo.CountLogsByInterval(sf, query.Interval)
|
ar, err = t.lo.CountLogsByInterval(sf, query.Interval)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if len(namespaceCreateTimeMap) == 0 {
|
if noHit {
|
||||||
ar.Logs = &loggingclient.Logs{}
|
ar.Logs = &loggingclient.Logs{}
|
||||||
} else {
|
} else {
|
||||||
ar, err = t.lo.SearchLogs(sf, query.From, query.Size, query.Sort)
|
ar, err = t.lo.SearchLogs(sf, query.From, query.Size, query.Sort)
|
||||||
@@ -741,9 +774,7 @@ func (t *tenantOperator) QueryLogs(user user.Info, query *loggingv1alpha2.Query)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *tenantOperator) ExportLogs(user user.Info, query *loggingv1alpha2.Query, writer io.Writer) error {
|
func (t *tenantOperator) ExportLogs(user user.Info, query *loggingv1alpha2.Query, writer io.Writer) error {
|
||||||
iNamespaces, err := t.listIntersectedNamespaces(
|
iNamespaces, err := t.listIntersectedNamespaces(nil, nil,
|
||||||
stringutils.Split(query.WorkspaceFilter, ","),
|
|
||||||
stringutils.Split(query.WorkspaceSearch, ","),
|
|
||||||
stringutils.Split(query.NamespaceFilter, ","),
|
stringutils.Split(query.NamespaceFilter, ","),
|
||||||
stringutils.Split(query.NamespaceSearch, ","))
|
stringutils.Split(query.NamespaceSearch, ","))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -751,7 +782,37 @@ func (t *tenantOperator) ExportLogs(user user.Info, query *loggingv1alpha2.Query
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
namespaceCreateTimeMap := make(map[string]time.Time)
|
namespaceCreateTimeMap := make(map[string]*time.Time)
|
||||||
|
|
||||||
|
var isGlobalAdmin bool
|
||||||
|
|
||||||
|
// If it is a global admin, the user can view logs from any namespace.
|
||||||
|
podLogs := authorizer.AttributesRecord{
|
||||||
|
User: user,
|
||||||
|
Verb: "get",
|
||||||
|
APIGroup: "",
|
||||||
|
APIVersion: "v1",
|
||||||
|
Resource: "pods",
|
||||||
|
Subresource: "log",
|
||||||
|
ResourceRequest: true,
|
||||||
|
ResourceScope: request.ClusterScope,
|
||||||
|
}
|
||||||
|
decision, _, err := t.authorizer.Authorize(podLogs)
|
||||||
|
if err != nil {
|
||||||
|
klog.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if decision == authorizer.DecisionAllow {
|
||||||
|
isGlobalAdmin = true
|
||||||
|
if query.NamespaceFilter != "" || query.NamespaceSearch != "" {
|
||||||
|
for _, ns := range iNamespaces {
|
||||||
|
namespaceCreateTimeMap[ns.Name] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is a regular user, this user can only view logs of namespaces the user belongs to.
|
||||||
|
if !isGlobalAdmin {
|
||||||
for _, ns := range iNamespaces {
|
for _, ns := range iNamespaces {
|
||||||
podLogs := authorizer.AttributesRecord{
|
podLogs := authorizer.AttributesRecord{
|
||||||
User: user,
|
User: user,
|
||||||
@@ -770,7 +831,8 @@ func (t *tenantOperator) ExportLogs(user user.Info, query *loggingv1alpha2.Query
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if decision == authorizer.DecisionAllow {
|
if decision == authorizer.DecisionAllow {
|
||||||
namespaceCreateTimeMap[ns.Name] = ns.CreationTimestamp.Time
|
namespaceCreateTimeMap[ns.Name] = &ns.CreationTimestamp.Time
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,7 +849,10 @@ func (t *tenantOperator) ExportLogs(user user.Info, query *loggingv1alpha2.Query
|
|||||||
Endtime: query.EndTime,
|
Endtime: query.EndTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(namespaceCreateTimeMap) == 0 {
|
noHit := !isGlobalAdmin && len(namespaceCreateTimeMap) == 0 ||
|
||||||
|
isGlobalAdmin && len(namespaceCreateTimeMap) == 0 && (query.NamespaceFilter != "" || query.NamespaceSearch != "")
|
||||||
|
|
||||||
|
if noHit {
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return t.lo.ExportLogs(sf, writer)
|
return t.lo.ExportLogs(sf, writer)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"github.com/json-iterator/go"
|
"github.com/json-iterator/go"
|
||||||
"k8s.io/klog"
|
"k8s.io/klog"
|
||||||
"kubesphere.io/kubesphere/pkg/simple/client/logging"
|
"kubesphere.io/kubesphere/pkg/simple/client/logging"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -36,7 +35,9 @@ func (bb *bodyBuilder) mainBool(sf logging.SearchFilter) *bodyBuilder {
|
|||||||
if len(sf.NamespaceFilter) != 0 {
|
if len(sf.NamespaceFilter) != 0 {
|
||||||
var b Bool
|
var b Bool
|
||||||
for ns := range sf.NamespaceFilter {
|
for ns := range sf.NamespaceFilter {
|
||||||
match := Match{
|
var match Match
|
||||||
|
if ct := sf.NamespaceFilter[ns]; ct != nil {
|
||||||
|
match = Match{
|
||||||
Bool: &Bool{
|
Bool: &Bool{
|
||||||
Filter: []Match{
|
Filter: []Match{
|
||||||
{
|
{
|
||||||
@@ -47,13 +48,26 @@ func (bb *bodyBuilder) mainBool(sf logging.SearchFilter) *bodyBuilder {
|
|||||||
{
|
{
|
||||||
Range: &Range{
|
Range: &Range{
|
||||||
Time: &Time{
|
Time: &Time{
|
||||||
Gte: func() *time.Time { t := sf.NamespaceFilter[ns]; return &t }(),
|
Gte: ct,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
match = Match{
|
||||||
|
Bool: &Bool{
|
||||||
|
Filter: []Match{
|
||||||
|
{
|
||||||
|
MatchPhrase: map[string]string{
|
||||||
|
"kubernetes.namespace_name.keyword": ns,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
b.Should = append(b.Should, match)
|
b.Should = append(b.Should, match)
|
||||||
}
|
}
|
||||||
b.MinimumShouldMatch = 1
|
b.MinimumShouldMatch = 1
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ func TestMainBool(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
filter: logging.SearchFilter{
|
filter: logging.SearchFilter{
|
||||||
NamespaceFilter: map[string]time.Time{
|
NamespaceFilter: map[string]*time.Time{
|
||||||
"default": time.Unix(1589981934, 0),
|
"default": func() *time.Time { t := time.Unix(1589981934, 0); return &t }(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: "api_body_1.json",
|
expected: "api_body_1.json",
|
||||||
@@ -51,6 +51,14 @@ func TestMainBool(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expected: "api_body_7.json",
|
expected: "api_body_7.json",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
filter: logging.SearchFilter{
|
||||||
|
NamespaceFilter: map[string]*time.Time{
|
||||||
|
"default": nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "api_body_8.json",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|||||||
26
pkg/simple/client/logging/elasticsearch/testdata/api_body_8.json
vendored
Normal file
26
pkg/simple/client/logging/elasticsearch/testdata/api_body_8.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"query":{
|
||||||
|
"bool":{
|
||||||
|
"filter":[
|
||||||
|
{
|
||||||
|
"bool":{
|
||||||
|
"should":[
|
||||||
|
{
|
||||||
|
"bool":{
|
||||||
|
"filter":[
|
||||||
|
{
|
||||||
|
"match_phrase":{
|
||||||
|
"kubernetes.namespace_name.keyword":"default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minimum_should_match":1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ type SearchFilter struct {
|
|||||||
// To prevent disclosing archived logs of a reopened namespace,
|
// To prevent disclosing archived logs of a reopened namespace,
|
||||||
// NamespaceFilter records the namespace creation time.
|
// NamespaceFilter records the namespace creation time.
|
||||||
// Any query to this namespace must begin after its creation.
|
// Any query to this namespace must begin after its creation.
|
||||||
NamespaceFilter map[string]time.Time
|
NamespaceFilter map[string]*time.Time
|
||||||
WorkloadSearch []string
|
WorkloadSearch []string
|
||||||
WorkloadFilter []string
|
WorkloadFilter []string
|
||||||
PodSearch []string
|
PodSearch []string
|
||||||
|
|||||||
Reference in New Issue
Block a user