-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3a9c388
commit 3c69d06
Showing
10 changed files
with
1,390 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,5 @@ | |
|
||
# Dependency directories (remove the comment below to include it) | ||
# vendor/ | ||
|
||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
FROM golang:1.13 as builder | ||
WORKDIR /workspace | ||
COPY go.mod go.mod | ||
COPY go.sum go.sum | ||
RUN go mod download | ||
COPY main.go main.go | ||
COPY webserver webserver | ||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o capsule-ns-filter main.go | ||
|
||
FROM gcr.io/distroless/static:nonroot | ||
WORKDIR / | ||
COPY --from=builder /workspace/capsule-ns-filter . | ||
USER nonroot:nonroot | ||
|
||
ENTRYPOINT ["/capsule-ns-filter"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,50 @@ | ||
# capsule-ns-filter | ||
PoC for Namespace aggregation for Tenant owners | ||
|
||
PoC for Namespace aggregation for Tenant owners. | ||
|
||
## The problem | ||
|
||
This is an add-on for [Capsule](https://github.com/clastix/capsule), a | ||
multi-tenant Kubernetes Operator that provides multi-tenancy in Kubernetes. | ||
|
||
Tl;dr; the _Tenant Owner_ is not able to list their Namespace resources: | ||
|
||
``` | ||
# kubectl get ns | ||
Error from server (Forbidden): namespaces is forbidden: User "alice" cannot list resource "namespaces" in API group "" at the cluster scope | ||
``` | ||
|
||
The reason, as the error message reported, is that the _list_ action is | ||
available only at Cluster-Scope. | ||
|
||
This project is a simple reverse proxy intercepting the Kubernetes | ||
`api/v1/namespaces` endpoint in order to filter according to the Capsule | ||
business logic only the available Namespace resources assigned to the | ||
requester. | ||
|
||
All other endpoints are proxied against the original Kubernetes API endpoint | ||
using the same request, so no side-effects are expected. | ||
|
||
## Installation | ||
|
||
`capsule-ns-filter` doesn't need to have `cluster-admin` _RoleBinding_ | ||
although all read verbs (`GET`, `LIST`, `WATCH`) against the following | ||
resources are mandatory: | ||
|
||
- `namespaces` | ||
- `tenants.capsule.clastix.io` | ||
|
||
## FAQ | ||
|
||
### Does it work with kubectl? | ||
|
||
That's a feature we're working on. | ||
|
||
<!-- TODO: track down feature with GH issues --> | ||
|
||
### Does it work with OpenShift Console? | ||
|
||
Actually, tested only with the _3.11_ release: with some hacks it can do the | ||
job. | ||
|
||
<!-- TODO: document with further details --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
module github.com/clastix/capsule-ns-filter | ||
|
||
go 1.13 | ||
|
||
require ( | ||
github.com/clastix/capsule v0.0.0-20200915134831-c75f773fc622 | ||
github.com/dgrijalva/jwt-go v3.2.0+incompatible | ||
github.com/google/go-cmp v0.5.2 // indirect | ||
github.com/google/gofuzz v1.2.0 // indirect | ||
github.com/gorilla/websocket v1.4.0 | ||
github.com/imdario/mergo v0.3.11 // indirect | ||
github.com/stretchr/testify v1.6.1 // indirect | ||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect | ||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect | ||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect | ||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect | ||
k8s.io/api v0.19.0-beta.2 | ||
k8s.io/apimachinery v0.19.0-beta.2 | ||
k8s.io/client-go v0.19.0-beta.2 | ||
k8s.io/utils v0.0.0-20200912215256-4140de9c8800 // indirect | ||
sigs.k8s.io/controller-runtime v0.6.2 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"fmt" | ||
"os" | ||
|
||
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1" | ||
"github.com/clastix/capsule/pkg/indexer/tenant" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||
clientgoscheme "k8s.io/client-go/kubernetes/scheme" | ||
ctrl "sigs.k8s.io/controller-runtime" | ||
"sigs.k8s.io/controller-runtime/pkg/log/zap" | ||
"sigs.k8s.io/controller-runtime/pkg/manager" | ||
|
||
"github.com/clastix/capsule-ns-filter/webserver" | ||
) | ||
|
||
var ( | ||
scheme = runtime.NewScheme() | ||
log = ctrl.Log.WithName("main") | ||
) | ||
|
||
func init() { | ||
utilruntime.Must(clientgoscheme.AddToScheme(scheme)) | ||
utilruntime.Must(capsulev1alpha1.AddToScheme(scheme)) | ||
} | ||
|
||
func main() { | ||
var err error | ||
var mgr ctrl.Manager | ||
|
||
fs := flag.NewFlagSet("filter", flag.ExitOnError) | ||
listeningPort := fs.Uint("listening-port", 9001, "HTTP port the proxy listens to") | ||
k8sControlPlaneUrl := fs.String("k8s-control-plane-url", "https://kubernetes.default.svc", "Kubernetes control plane URL") | ||
capsuleUserGroup := fs.String("capsule-user-group", "clastix.capsule.io", "The Capsule User Group eligible to create Namespace for Tenant resources") | ||
err = fs.Parse(os.Args[1:]) | ||
|
||
opts := zap.Options{} | ||
opts.BindFlags(flag.CommandLine) | ||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) | ||
|
||
if err != nil { | ||
log.Error(err, "cannot parse flags") | ||
os.Exit(1) | ||
} | ||
|
||
log.Info("---") | ||
log.Info(fmt.Sprintf("Manager will listen to port %d", *listeningPort)) | ||
log.Info(fmt.Sprintf("Connecting to the Kubernete API Server listening on %s", *k8sControlPlaneUrl)) | ||
log.Info(fmt.Sprintf("The selected Capsule User Group is %s", *capsuleUserGroup)) | ||
log.Info("---") | ||
|
||
log.Info("Creating the manager") | ||
mgr, err = ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ | ||
Scheme: scheme, | ||
HealthProbeBindAddress: ":8081", | ||
}) | ||
if err != nil { | ||
log.Error(err, "cannot create new Manager") | ||
os.Exit(1) | ||
} | ||
|
||
log.Info("Creating the Field Indexer") | ||
ow := tenant.OwnerReference{} | ||
err = mgr.GetFieldIndexer().IndexField(context.TODO(), ow.Object(), ow.Field(), ow.Func()) | ||
if err != nil { | ||
log.Error(err, "cannot create new Field Indexer") | ||
os.Exit(1) | ||
} | ||
|
||
var r manager.Runnable | ||
log.Info("Creating the NamespaceFilter runner") | ||
r, err = webserver.NewKubeFilter(*listeningPort, *k8sControlPlaneUrl, *capsuleUserGroup, ctrl.GetConfigOrDie()) | ||
if err != nil { | ||
log.Error(err, "cannot create NamespaceFilter runner") | ||
os.Exit(1) | ||
} | ||
|
||
log.Info("Adding the NamespaceFilter runner to the Manager") | ||
err = mgr.Add(r) | ||
if err != nil { | ||
log.Error(err, "cannot add NameSpaceFilter as Runnable") | ||
os.Exit(1) | ||
} | ||
|
||
log.Info("Starting the Manager") | ||
err = mgr.Start(ctrl.SetupSignalHandler()) | ||
if err != nil { | ||
log.Error(err, "cannot start the Manager") | ||
os.Exit(1) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package webserver | ||
|
||
import ( | ||
"strings" | ||
) | ||
|
||
type NamespaceList []string | ||
|
||
func (n NamespaceList) Len() int { | ||
return len(n) | ||
} | ||
|
||
func (n NamespaceList) Less(i, j int) bool { | ||
return strings.ToLower(n[i]) < strings.ToLower(n[j]) | ||
} | ||
|
||
func (n NamespaceList) Swap(i, j int) { | ||
n[i], n[j] = n[j], n[i] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package webserver | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/clastix/capsule/pkg/utils" | ||
"github.com/dgrijalva/jwt-go" | ||
) | ||
|
||
type Request interface { | ||
IsUserInGroup(groupName string) (bool, error) | ||
GetUserName() (string, error) | ||
} | ||
|
||
type httpRequest struct { | ||
*http.Request | ||
} | ||
|
||
func (h httpRequest) getJwtClaims() jwt.MapClaims { | ||
parser := jwt.Parser{ | ||
SkipClaimsValidation: true, | ||
} | ||
token, _, err := parser.ParseUnverified(strings.Replace(h.Header.Get("Authorization"), "Bearer ", "", -1), jwt.MapClaims{}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
return token.Claims.(jwt.MapClaims) | ||
} | ||
|
||
func (h httpRequest) IsUserInGroup(groupName string) (bool, error) { | ||
claims := h.getJwtClaims() | ||
g, ok := claims["groups"] | ||
if !ok { | ||
return false, fmt.Errorf("missing groups claim in JWT") | ||
} | ||
var groups []string | ||
for _, v := range g.([]interface{}) { | ||
groups = append(groups, v.(string)) | ||
} | ||
return utils.UserGroupList(groups).IsInCapsuleGroup(groupName), nil | ||
} | ||
|
||
func (h httpRequest) GetUserName() (string, error) { | ||
claims := h.getJwtClaims() | ||
username, ok := claims["preferred_username"] | ||
if !ok { | ||
return "", fmt.Errorf("missing groups claim in JWT") | ||
} | ||
return username.(string), nil | ||
} | ||
|
||
func NewHttpRequest(request *http.Request) Request { | ||
return &httpRequest{Request: request} | ||
} |
Oops, something went wrong.