From 70d0d5fe180b21d28df0f1dd320f067debf7fd52 Mon Sep 17 00:00:00 2001 From: huanggze Date: Tue, 28 Jul 2020 11:55:17 +0800 Subject: [PATCH] Allow global admins to view deleted namespace logs Signed-off-by: huanggze --- pkg/api/logging/v1alpha2/types.go | 4 - pkg/kapis/tenant/v1alpha2/register.go | 2 - pkg/models/tenant/tenant.go | 161 ++++++++++++------ .../client/logging/elasticsearch/api_body.go | 40 +++-- .../logging/elasticsearch/api_body_test.go | 12 +- .../elasticsearch/testdata/api_body_8.json | 26 +++ pkg/simple/client/logging/interface.go | 2 +- 7 files changed, 177 insertions(+), 70 deletions(-) create mode 100644 pkg/simple/client/logging/elasticsearch/testdata/api_body_8.json diff --git a/pkg/api/logging/v1alpha2/types.go b/pkg/api/logging/v1alpha2/types.go index a63e9f46d..eaa8b1b54 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 956013e0a..82365826b 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 b8bbb27a8..11cc5510a 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, + 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 + } } - 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 + } + + // 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, + 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 + } } - decision, _, err := t.authorizer.Authorize(podLogs) - if err != nil { - klog.Error(err) - return err - } - 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 823d2ca97..ade12f5db 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: func() *time.Time { t := sf.NamespaceFilter[ns]; return &t }(), + { + Range: &Range{ + Time: &Time{ + Gte: ct, + }, }, }, }, }, - }, + } + } 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 3ab5c777e..8514d9d1f 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 000000000..a911891c1 --- /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 0ada45cb0..03ee27fde 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