diff --git a/pkg/api/logging/v1alpha2/types.go b/pkg/api/logging/v1alpha2/types.go index a63e9f46d9f7a88a38dce9d938670c9bdafcd371..eaa8b1b540754d2a7574f4962454a1fe08d2fef6 100644 --- a/pkg/api/logging/v1alpha2/types.go +++ b/pkg/api/logging/v1alpha2/types.go @@ -27,8 +27,6 @@ type APIResponse struct { type Query struct { Operation string - WorkspaceFilter string - WorkspaceSearch string NamespaceFilter string NamespaceSearch string WorkloadFilter string @@ -49,8 +47,6 @@ type Query struct { func ParseQueryParameter(req *restful.Request) (*Query, error) { var q Query q.Operation = req.QueryParameter("operation") - q.WorkspaceFilter = req.QueryParameter("workspaces") - q.WorkspaceSearch = req.QueryParameter("workspace_query") q.NamespaceFilter = req.QueryParameter("namespaces") q.NamespaceSearch = req.QueryParameter("namespace_query") q.WorkloadFilter = req.QueryParameter("workloads") diff --git a/pkg/kapis/tenant/v1alpha2/register.go b/pkg/kapis/tenant/v1alpha2/register.go index 956013e0aaa1ae4f720b6370f24e606e69fe3f73..82365826b181a7687559ee53ed07410d36224c53 100644 --- a/pkg/kapis/tenant/v1alpha2/register.go +++ b/pkg/kapis/tenant/v1alpha2/register.go @@ -192,8 +192,6 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s To(handler.QueryLogs). 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("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("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)). diff --git a/pkg/models/tenant/tenant.go b/pkg/models/tenant/tenant.go index b8bbb27a809ef831d65d7eb5404ea2d41f0894a2..11cc5510a0860e6e068b58d86f77c2116bd56da5 100644 --- a/pkg/models/tenant/tenant.go +++ b/pkg/models/tenant/tenant.go @@ -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. // 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. +// 5. If ALL of the filters above are empty, returns all namespaces. func (t *tenantOperator) listIntersectedNamespaces(workspaces, workspaceSubstrs, namespaces, namespaceSubstrs []string) ([]*corev1.Namespace, error) { 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) { - iNamespaces, err := t.listIntersectedNamespaces( - stringutils.Split(query.WorkspaceFilter, ","), - stringutils.Split(query.WorkspaceSearch, ","), + iNamespaces, err := t.listIntersectedNamespaces(nil, nil, stringutils.Split(query.NamespaceFilter, ","), stringutils.Split(query.NamespaceSearch, ",")) if err != nil { @@ -680,26 +679,57 @@ func (t *tenantOperator) QueryLogs(user user.Info, query *loggingv1alpha2.Query) return nil, err } - namespaceCreateTimeMap := make(map[string]time.Time) - for _, ns := range iNamespaces { - podLogs := authorizer.AttributesRecord{ - User: user, - Verb: "get", - APIGroup: "", - APIVersion: "v1", - Namespace: ns.Name, - Resource: "pods", - Subresource: "log", - ResourceRequest: true, - ResourceScope: request.NamespaceScope, - } - decision, _, err := t.authorizer.Authorize(podLogs) - if err != nil { - klog.Error(err) - return nil, err + 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 decision == authorizer.DecisionAllow { - namespaceCreateTimeMap[ns.Name] = ns.CreationTimestamp.Time + } + + // If it is a regular user, this user can only view logs of namespaces the user belongs to. + if !isGlobalAdmin { + for _, ns := range iNamespaces { + podLogs := authorizer.AttributesRecord{ + User: user, + Verb: "get", + APIGroup: "", + APIVersion: "v1", + Namespace: ns.Name, + Resource: "pods", + Subresource: "log", + ResourceRequest: true, + ResourceScope: request.NamespaceScope, + } + decision, _, err := t.authorizer.Authorize(podLogs) + if err != nil { + klog.Error(err) + return nil, err + } + if decision == authorizer.DecisionAllow { + namespaceCreateTimeMap[ns.Name] = &ns.CreationTimestamp.Time + } } } @@ -717,21 +747,24 @@ func (t *tenantOperator) QueryLogs(user user.Info, query *loggingv1alpha2.Query) } var ar loggingv1alpha2.APIResponse + noHit := !isGlobalAdmin && len(namespaceCreateTimeMap) == 0 || + isGlobalAdmin && len(namespaceCreateTimeMap) == 0 && (query.NamespaceFilter != "" || query.NamespaceSearch != "") + switch query.Operation { case loggingv1alpha2.OperationStatistics: - if len(namespaceCreateTimeMap) == 0 { + if noHit { ar.Statistics = &loggingclient.Statistics{} } else { ar, err = t.lo.GetCurrentStats(sf) } case loggingv1alpha2.OperationHistogram: - if len(namespaceCreateTimeMap) == 0 { + if noHit { ar.Histogram = &loggingclient.Histogram{} } else { ar, err = t.lo.CountLogsByInterval(sf, query.Interval) } default: - if len(namespaceCreateTimeMap) == 0 { + if noHit { ar.Logs = &loggingclient.Logs{} } else { 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 { - iNamespaces, err := t.listIntersectedNamespaces( - stringutils.Split(query.WorkspaceFilter, ","), - stringutils.Split(query.WorkspaceSearch, ","), + iNamespaces, err := t.listIntersectedNamespaces(nil, nil, stringutils.Split(query.NamespaceFilter, ","), stringutils.Split(query.NamespaceSearch, ",")) if err != nil { @@ -751,26 +782,57 @@ func (t *tenantOperator) ExportLogs(user user.Info, query *loggingv1alpha2.Query return err } - namespaceCreateTimeMap := make(map[string]time.Time) - for _, ns := range iNamespaces { - podLogs := authorizer.AttributesRecord{ - User: user, - Verb: "get", - APIGroup: "", - APIVersion: "v1", - Namespace: ns.Name, - Resource: "pods", - Subresource: "log", - ResourceRequest: true, - ResourceScope: request.NamespaceScope, - } - decision, _, err := t.authorizer.Authorize(podLogs) - if err != nil { - klog.Error(err) - return err + 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 decision == authorizer.DecisionAllow { - namespaceCreateTimeMap[ns.Name] = ns.CreationTimestamp.Time + } + + // If it is a regular user, this user can only view logs of namespaces the user belongs to. + if !isGlobalAdmin { + for _, ns := range iNamespaces { + podLogs := authorizer.AttributesRecord{ + User: user, + Verb: "get", + APIGroup: "", + APIVersion: "v1", + Namespace: ns.Name, + Resource: "pods", + Subresource: "log", + ResourceRequest: true, + ResourceScope: request.NamespaceScope, + } + decision, _, err := t.authorizer.Authorize(podLogs) + if err != nil { + klog.Error(err) + return err + } + if decision == authorizer.DecisionAllow { + namespaceCreateTimeMap[ns.Name] = &ns.CreationTimestamp.Time + } } } @@ -787,7 +849,10 @@ func (t *tenantOperator) ExportLogs(user user.Info, query *loggingv1alpha2.Query Endtime: query.EndTime, } - if len(namespaceCreateTimeMap) == 0 { + noHit := !isGlobalAdmin && len(namespaceCreateTimeMap) == 0 || + isGlobalAdmin && len(namespaceCreateTimeMap) == 0 && (query.NamespaceFilter != "" || query.NamespaceSearch != "") + + if noHit { return nil } else { return t.lo.ExportLogs(sf, writer) diff --git a/pkg/simple/client/logging/elasticsearch/api_body.go b/pkg/simple/client/logging/elasticsearch/api_body.go index 823d2ca97e9c221320e13d8fed3546adad54575b..ade12f5db9586f04f428d30ab9dacc2f2dd9653d 100644 --- a/pkg/simple/client/logging/elasticsearch/api_body.go +++ b/pkg/simple/client/logging/elasticsearch/api_body.go @@ -5,7 +5,6 @@ import ( "github.com/json-iterator/go" "k8s.io/klog" "kubesphere.io/kubesphere/pkg/simple/client/logging" - "time" ) const ( @@ -36,23 +35,38 @@ func (bb *bodyBuilder) mainBool(sf logging.SearchFilter) *bodyBuilder { if len(sf.NamespaceFilter) != 0 { var b Bool for ns := range sf.NamespaceFilter { - match := Match{ - Bool: &Bool{ - Filter: []Match{ - { - MatchPhrase: map[string]string{ - "kubernetes.namespace_name.keyword": ns, + var match Match + if ct := sf.NamespaceFilter[ns]; ct != nil { + match = Match{ + Bool: &Bool{ + Filter: []Match{ + { + MatchPhrase: map[string]string{ + "kubernetes.namespace_name.keyword": ns, + }, + }, + { + Range: &Range{ + Time: &Time{ + Gte: ct, + }, + }, }, }, - { - Range: &Range{ - Time: &Time{ - Gte: func() *time.Time { t := sf.NamespaceFilter[ns]; return &t }(), + }, + } + } else { + match = Match{ + Bool: &Bool{ + Filter: []Match{ + { + MatchPhrase: map[string]string{ + "kubernetes.namespace_name.keyword": ns, }, }, }, }, - }, + } } b.Should = append(b.Should, match) } diff --git a/pkg/simple/client/logging/elasticsearch/api_body_test.go b/pkg/simple/client/logging/elasticsearch/api_body_test.go index 3ab5c777e71c438df338ef423f9a6fb2bdb2d29c..8514d9d1fe4f735df56f310a181515efd13f73b6 100644 --- a/pkg/simple/client/logging/elasticsearch/api_body_test.go +++ b/pkg/simple/client/logging/elasticsearch/api_body_test.go @@ -16,8 +16,8 @@ func TestMainBool(t *testing.T) { }{ { filter: logging.SearchFilter{ - NamespaceFilter: map[string]time.Time{ - "default": time.Unix(1589981934, 0), + NamespaceFilter: map[string]*time.Time{ + "default": func() *time.Time { t := time.Unix(1589981934, 0); return &t }(), }, }, expected: "api_body_1.json", @@ -51,6 +51,14 @@ func TestMainBool(t *testing.T) { }, 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 { diff --git a/pkg/simple/client/logging/elasticsearch/testdata/api_body_8.json b/pkg/simple/client/logging/elasticsearch/testdata/api_body_8.json new file mode 100644 index 0000000000000000000000000000000000000000..a911891c111d277510a986e26a348c8f56e78025 --- /dev/null +++ b/pkg/simple/client/logging/elasticsearch/testdata/api_body_8.json @@ -0,0 +1,26 @@ +{ + "query":{ + "bool":{ + "filter":[ + { + "bool":{ + "should":[ + { + "bool":{ + "filter":[ + { + "match_phrase":{ + "kubernetes.namespace_name.keyword":"default" + } + } + ] + } + } + ], + "minimum_should_match":1 + } + } + ] + } + } +} \ No newline at end of file diff --git a/pkg/simple/client/logging/interface.go b/pkg/simple/client/logging/interface.go index 0ada45cb01a915f8adb8dd4d7ca93fcc3a6f732a..03ee27fded7556a29a2f14bcfdfcd897a75a32e0 100644 --- a/pkg/simple/client/logging/interface.go +++ b/pkg/simple/client/logging/interface.go @@ -51,7 +51,7 @@ type SearchFilter struct { // To prevent disclosing archived logs of a reopened namespace, // NamespaceFilter records the namespace creation time. // Any query to this namespace must begin after its creation. - NamespaceFilter map[string]time.Time + NamespaceFilter map[string]*time.Time WorkloadSearch []string WorkloadFilter []string PodSearch []string