Skip to content
This repository has been archived by the owner on Nov 27, 2024. It is now read-only.

Commit

Permalink
rest: use UserSignup in request authentication (#180)
Browse files Browse the repository at this point in the history
Authentication is performed by checking the existence of a valid
UserSignup. UserSignup matching is performed on the JWT's `sub` field.

Signed-off-by: Francesco Ilario <filario@redhat.com>
Co-authored-by: Andy Sadler <ansadler@redhat.com>
  • Loading branch information
filariow and sadlerap authored Jun 4, 2024
1 parent 580077f commit a3c5004
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 37 deletions.
4 changes: 3 additions & 1 deletion e2e/step/user/user_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func OnboardUser(ctx context.Context, cli cli.Cli, namespace, name string) (*too
h := md5.New()
h.Write([]byte(e))
eh := hex.EncodeToString(h.Sum(nil))
prefixedUsername := cli.EnsurePrefix(name)

u := toolchainv1alpha1.UserSignup{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -44,8 +45,9 @@ func OnboardUser(ctx context.Context, cli cli.Cli, namespace, name string) (*too
IdentityClaims: toolchainv1alpha1.IdentityClaimsEmbedded{
PropagatedClaims: toolchainv1alpha1.PropagatedClaims{
Email: e,
Sub: prefixedUsername,
},
PreferredUsername: cli.EnsurePrefix(name),
PreferredUsername: prefixedUsername,
},
States: []toolchainv1alpha1.UserSignupState{toolchainv1alpha1.UserSignupStateApproved},
},
Expand Down
13 changes: 13 additions & 0 deletions server/config/rbac/clusterrole_usersignup_reader.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: usersignup-reader
rules:
- apiGroups:
- toolchain.dev.openshift.com
resources:
- usersignups
verbs:
- list
- get
- watch
12 changes: 12 additions & 0 deletions server/config/rbac/clusterrolebinding_usersignup_reader.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: rest-api-server:usersignup-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: usersignup-reader
subjects:
- kind: ServiceAccount
name: rest-api-server
namespace: system
2 changes: 2 additions & 0 deletions server/config/rbac/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- clusterrole_spacebinding_reader.yaml
- clusterrole_usersignup_reader.yaml
- clusterrole_workspace_reader.yaml
- clusterrolebinding_spacebinding_reader.yaml
- clusterrolebinding_usersignup_reader.yaml
- clusterrolebinding_workspace_reader.yaml
- serviceaccount.yaml
- ./clusterrole_user_impersonator.yaml
Expand Down
3 changes: 2 additions & 1 deletion server/core/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ package context
type ServerContextKey string

const (
UserKey ServerContextKey = "user"
UserSubKey ServerContextKey = "user-sub"
UserSignupComplaintNameKey ServerContextKey = "usersignup-complaintname"
)
2 changes: 1 addition & 1 deletion server/core/workspace/workspace_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func NewCreateWorkspaceHandler(creator WorkspaceCreator) *CreateWorkspaceHandler
}

func (h *CreateWorkspaceHandler) Handle(ctx context.Context, request CreateWorkspaceCommand) (*CreateWorkspaceResponse, error) {
u, ok := ctx.Value(ccontext.UserKey).(string)
u, ok := ctx.Value(ccontext.UserSignupComplaintNameKey).(string)
if !ok {
return nil, fmt.Errorf("unauthenticated request")
}
Expand Down
4 changes: 2 additions & 2 deletions server/core/workspace/workspace_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var _ = Describe("", func() {
It("should allow authenticated requests", func() {
// given
username := "foo"
ctx := context.WithValue(ctx, ccontext.UserKey, username)
ctx := context.WithValue(ctx, ccontext.UserSignupComplaintNameKey, username)
opts := &client.CreateOptions{}
creator.EXPECT().
CreateUserWorkspace(ctx, username, &request.Workspace, opts).
Expand All @@ -64,7 +64,7 @@ var _ = Describe("", func() {
It("should forward errors from the workspace creator", func() {
// given
username := "foo"
ctx := context.WithValue(ctx, ccontext.UserKey, username)
ctx := context.WithValue(ctx, ccontext.UserSignupComplaintNameKey, username)
opts := &client.CreateOptions{}
error := fmt.Errorf("Failed to create workspace!")
creator.EXPECT().
Expand Down
2 changes: 1 addition & 1 deletion server/core/workspace/workspace_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func NewListWorkspaceHandler(lister WorkspaceLister) *ListWorkspaceHandler {
func (h *ListWorkspaceHandler) Handle(ctx context.Context, query ListWorkspaceQuery) (*ListWorkspaceResponse, error) {
// authorization
// If required, implement here complex logic like multiple-domains filtering, etc
u, ok := ctx.Value(ccontext.UserKey).(string)
u, ok := ctx.Value(ccontext.UserSignupComplaintNameKey).(string)
if !ok {
return nil, fmt.Errorf("unauthenticated request")
}
Expand Down
2 changes: 1 addition & 1 deletion server/core/workspace/workspace_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func NewReadWorkspaceHandler(reader WorkspaceReader) *ReadWorkspaceHandler {
func (h *ReadWorkspaceHandler) Handle(ctx context.Context, query ReadWorkspaceQuery) (*ReadWorkspaceResponse, error) {
// authorization
// If required, implement here complex logic like multiple-domains filtering, etc
u, ok := ctx.Value(ccontext.UserKey).(string)
u, ok := ctx.Value(ccontext.UserSignupComplaintNameKey).(string)
if !ok {
return nil, fmt.Errorf("unauthenticated request")
}
Expand Down
2 changes: 1 addition & 1 deletion server/core/workspace/workspace_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func NewUpdateWorkspaceHandler(updater WorkspaceUpdater) *UpdateWorkspaceHandler
func (h *UpdateWorkspaceHandler) Handle(ctx context.Context, query UpdateWorkspaceCommand) (*UpdateWorkspaceResponse, error) {
// authorization
// If required, implement here complex logic like multiple-domains filtering, etc
u, ok := ctx.Value(ccontext.UserKey).(string)
u, ok := ctx.Value(ccontext.UserSignupComplaintNameKey).(string)
if !ok {
return nil, fmt.Errorf("unauthenticated request")
}
Expand Down
1 change: 1 addition & 0 deletions server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func run(l *slog.Logger) error {
s := rest.New(
l,
DefaultAddr,
crc,
workspace.NewReadWorkspaceHandler(c).Handle,
workspace.NewListWorkspaceHandler(c).Handle,
workspace.NewCreateWorkspaceHandler(writer).Handle,
Expand Down
4 changes: 4 additions & 0 deletions server/persistence/internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ func NewCache(ctx context.Context, cfg *rest.Config, workspacesNamespace, kubesa
if _, err := c.GetInformer(ctx, &toolchainv1alpha1.SpaceBinding{}); err != nil {
return nil, err
}
if _, err := c.GetInformer(ctx, &toolchainv1alpha1.UserSignup{}); err != nil {
return nil, err
}
if _, err := c.GetInformer(ctx, &workspacesv1alpha1.InternalWorkspace{}); err != nil {
return nil, err
}
Expand All @@ -55,6 +58,7 @@ func newCache(cfg *rest.Config, scheme *runtime.Scheme, mapper meta.RESTMapper,
Mapper: mapper,
ReaderFailOnMissingInformer: true,
ByObject: map[client.Object]cache.ByObject{
&toolchainv1alpha1.UserSignup{}: {Namespaces: map[string]cache.Config{kubesawNamespace: {}}},
&toolchainv1alpha1.SpaceBinding{}: {Namespaces: map[string]cache.Config{kubesawNamespace: {}}},
&workspacesv1alpha1.InternalWorkspace{}: {Namespaces: map[string]cache.Config{workspacesNamespace: {}}},
},
Expand Down
3 changes: 1 addition & 2 deletions server/persistence/iwclient/iwclient_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (c *Client) GetAsUser(
) error {
l := log.FromContext(ctx).With("key", key, "user", user)
l.Debug("retrieving InternalWorkspace")
w, err := c.fetchInternalWorkspaceByLabel(ctx, user, key.Owner, key.Name, nil)
w, err := c.fetchInternalWorkspaceByLabel(ctx, key.Owner, key.Name, nil)
if err != nil {
l.Error("error retrieving InternalWorkspace", "error", err)
return err
Expand Down Expand Up @@ -56,7 +56,6 @@ func (c *Client) GetAsUser(

func (c *Client) fetchInternalWorkspaceByLabel(
ctx context.Context,
user string,
owner string,
space string,
_ ...client.GetOption,
Expand Down
82 changes: 82 additions & 0 deletions server/rest/middleware/usersignup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package middleware

import (
"context"
"net/http"

"sigs.k8s.io/controller-runtime/pkg/cache"

ccontext "github.com/konflux-workspaces/workspaces/server/core/context"

toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
)

type UserSignupMiddleware struct {
cache cache.Cache
requireUserSignup bool

next http.Handler
}

func NewUserSignupMiddleware(next http.Handler, cache cache.Cache, requireUserSignup bool) *UserSignupMiddleware {
return &UserSignupMiddleware{
cache: cache,
requireUserSignup: requireUserSignup,

next: next,
}
}

func (m *UserSignupMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// retrieve User's JWT Sub
u, ok := r.Context().Value(ccontext.UserSubKey).(string)
if !ok {
m.next.ServeHTTP(w, r)
return
}

// retrieve UserSignup for given sub
us, err := m.lookupUserSignup(r.Context(), u)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

if us == nil {
w.WriteHeader(http.StatusForbidden)
if _, err := w.Write([]byte("user needs to sign in")); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
return
}

// user is waiting for approval
if us.Status.CompliantUsername == "" {
w.WriteHeader(http.StatusForbidden)
if _, err := w.Write([]byte("user is waiting for approval")); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
return
}

// TODO(@filariow): check if user is deactivated or banned

// inject the userSignup.ComplaintUsername
ctx := context.WithValue(r.Context(), ccontext.UserSignupComplaintNameKey, us.Status.CompliantUsername)
m.next.ServeHTTP(w, r.WithContext(ctx))
}

func (m *UserSignupMiddleware) lookupUserSignup(ctx context.Context, sub string) (*toolchainv1alpha1.UserSignup, error) {
uu := toolchainv1alpha1.UserSignupList{}
if err := m.cache.List(ctx, &uu); err != nil {
return nil, err
}

for _, u := range uu.Items {
if u.Spec.IdentityClaims.Sub == sub {
return &u, nil
}
}

return nil, nil
}
67 changes: 40 additions & 27 deletions server/rest/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"net/http"
"time"

"sigs.k8s.io/controller-runtime/pkg/cache"

ccontext "github.com/konflux-workspaces/workspaces/server/core/context"
"github.com/konflux-workspaces/workspaces/server/rest/marshal"
"github.com/konflux-workspaces/workspaces/server/rest/middleware"
Expand All @@ -20,28 +22,30 @@ const (
func New(
logger *slog.Logger,
addr string,
cache cache.Cache,
readHandle workspace.ReadWorkspaceQueryHandlerFunc,
listHandle workspace.ListWorkspaceQueryHandlerFunc,
createHandle workspace.CreateWorkspaceCreateHandlerFunc,
updateHandle workspace.UpdateWorkspaceCommandHandlerFunc,
) *http.Server {
return &http.Server{
Addr: addr,
Handler: buildServerHandler(logger, readHandle, listHandle, createHandle, updateHandle),
Handler: buildServerHandler(logger, cache, readHandle, listHandle, createHandle, updateHandle),
ReadHeaderTimeout: 3 * time.Second,
}
}

func buildServerHandler(
logger *slog.Logger,
cache cache.Cache,
readHandle workspace.ReadWorkspaceQueryHandlerFunc,
listHandle workspace.ListWorkspaceQueryHandlerFunc,
createHandle workspace.CreateWorkspaceCreateHandlerFunc,
updateHandle workspace.UpdateWorkspaceCommandHandlerFunc,
) http.Handler {
mux := http.NewServeMux()
addHealthz(mux)
addWorkspaces(mux, readHandle, listHandle, createHandle, updateHandle)
addWorkspaces(mux, cache, readHandle, listHandle, createHandle, updateHandle)
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
})
Expand All @@ -57,6 +61,7 @@ func buildServerHandler(

func addWorkspaces(
mux *http.ServeMux,
cache cache.Cache,
readHandle workspace.ReadWorkspaceQueryHandlerFunc,
listHandle workspace.ListWorkspaceQueryHandlerFunc,
postHandle workspace.CreateWorkspaceCreateHandlerFunc,
Expand All @@ -65,51 +70,59 @@ func addWorkspaces(
// Read
mux.Handle(fmt.Sprintf("GET %s/{name}", NamespacedWorkspacesPrefix),
withAuthHeaderInfo(
workspace.NewReadWorkspaceHandler(
workspace.MapReadWorkspaceHttp,
readHandle,
marshal.DefaultMarshalerProvider,
)))
withUserSignupAuth(cache, true,
workspace.NewReadWorkspaceHandler(
workspace.MapReadWorkspaceHttp,
readHandle,
marshal.DefaultMarshalerProvider,
))))

// List
lh := withAuthHeaderInfo(
workspace.NewListWorkspaceHandler(
workspace.MapListWorkspaceHttp,
listHandle,
marshal.DefaultMarshalerProvider,
marshal.DefaultUnmarshalerProvider,
),
)
withUserSignupAuth(cache, true,
workspace.NewListWorkspaceHandler(
workspace.MapListWorkspaceHttp,
listHandle,
marshal.DefaultMarshalerProvider,
marshal.DefaultUnmarshalerProvider,
),
))
mux.Handle(fmt.Sprintf("GET %s", WorkspacesPrefix), lh)
mux.Handle(fmt.Sprintf("GET %s", NamespacedWorkspacesPrefix), lh)

// Update
mux.Handle(fmt.Sprintf("PUT %s/{name}", NamespacedWorkspacesPrefix),
withAuthHeaderInfo(
workspace.NewUpdateWorkspaceHandler(
workspace.MapUpdateWorkspaceHttp,
updateHandle,
marshal.DefaultMarshalerProvider,
marshal.DefaultUnmarshalerProvider,
)))
withUserSignupAuth(cache, true,
workspace.NewUpdateWorkspaceHandler(
workspace.MapUpdateWorkspaceHttp,
updateHandle,
marshal.DefaultMarshalerProvider,
marshal.DefaultUnmarshalerProvider,
))))

// Create
mux.Handle(fmt.Sprintf("POST %s", NamespacedWorkspacesPrefix),
withAuthHeaderInfo(
workspace.NewPostWorkspaceHandler(
workspace.MapPostWorkspaceHttp,
postHandle,
marshal.DefaultMarshalerProvider,
marshal.DefaultUnmarshalerProvider,
)))
withUserSignupAuth(cache, true,
workspace.NewPostWorkspaceHandler(
workspace.MapPostWorkspaceHttp,
postHandle,
marshal.DefaultMarshalerProvider,
marshal.DefaultUnmarshalerProvider,
))))
}

func withAuthHeaderInfo(next http.Handler) http.Handler {
return middleware.NewHeaderInfoMiddleware(next, map[string]interface{}{
"X-Subject": ccontext.UserKey,
"X-Subject": ccontext.UserSubKey,
})
}

func withUserSignupAuth(cache cache.Cache, required bool, next http.Handler) http.Handler {
return middleware.NewUserSignupMiddleware(next, cache, required)
}

func addHealthz(mux *http.ServeMux) {
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write([]byte("alive")); err != nil {
Expand Down

0 comments on commit a3c5004

Please sign in to comment.