Skip to content

Commit 8472785

Browse files
RHOAIENG-11155: Better explanation of 'Authorize Access' UI (#449)
* Add methods to handle OAuth client secret generation - Add volumes to store the oauth-client configuration; - Add extra parameters to container creation, including --client-id, --client-secret and --scope; - Add method to generate the secret randomly when authenticating using OAuth; - Add link between the notebook's route, user's generated oauth secret and OAuthClient config; * Add RBAC role to have access to OAuthClient objects * Fix pointer.Int32Ptr deprecation on oauth-client test * Add OAuthClient role to odh-notebook-controller's kubebuilder config * Change error message on OAuth Client creation error * Add validation when getting route from OAuthClient reconciler * Fix pointer.Int32Ptr deprecation in webhook * Fix linter complaints about r.Client presence * Remove oauthClient ownership code regarding rhods-operator * Add proper description to empty comments * Add label for notebook-owner to the oauthclient object * Add ForceOwnership back to OAuthClient Patch Co-authored-by: Harshad Reddy Nalla <[email protected]> * Fix linter complaints about last commits --------- Co-authored-by: Harshad Reddy Nalla <[email protected]>
1 parent 07f5a24 commit 8472785

File tree

6 files changed

+225
-13
lines changed

6 files changed

+225
-13
lines changed

components/odh-notebook-controller/config/rbac/role.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ rules:
6565
- patch
6666
- update
6767
- watch
68+
- apiGroups:
69+
- oauth.openshift.io
70+
resources:
71+
- oauthclients
72+
verbs:
73+
- create
74+
- get
75+
- list
76+
- patch
77+
- update
78+
- watch
6879
- apiGroups:
6980
- rbac.authorization.k8s.io
7081
resources:

components/odh-notebook-controller/controllers/notebook_controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type OpenshiftNotebookReconciler struct {
7373
// +kubebuilder:rbac:groups="",resources=services;serviceaccounts;secrets;configmaps,verbs=get;list;watch;create;update;patch
7474
// +kubebuilder:rbac:groups=config.openshift.io,resources=proxies,verbs=get;list;watch
7575
// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;patch
76+
// +kubebuilder:rbac:groups=oauth.openshift.io,resources=oauthclients,verbs=get;list;watch;create;update;patch
7677
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;watch;create;update;patch
7778
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete
7879
// +kubebuilder:rbac:groups="image.openshift.io",resources=imagestreams,verbs=list;get
@@ -234,6 +235,12 @@ func (r *OpenshiftNotebookReconciler) Reconcile(ctx context.Context, req ctrl.Re
234235
if err != nil {
235236
return ctrl.Result{}, err
236237
}
238+
239+
// Call the OAuthClient reconciler
240+
err = r.ReconcileOAuthClient(notebook, ctx)
241+
if err != nil {
242+
return ctrl.Result{}, err
243+
}
237244
} else {
238245
// Call the route reconciler (see notebook_route.go file)
239246
err = r.ReconcileRoute(notebook, ctx)

components/odh-notebook-controller/controllers/notebook_controller_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,15 @@ var _ = Describe("The Openshift Notebook controller", func() {
630630
},
631631
},
632632
},
633+
{
634+
Name: "oauth-client",
635+
VolumeSource: corev1.VolumeSource{
636+
Secret: &corev1.SecretVolumeSource{
637+
SecretName: Name + "-oauth-client",
638+
DefaultMode: ptr.To[int32](420),
639+
},
640+
},
641+
},
633642
{
634643
Name: "tls-certificates",
635644
VolumeSource: corev1.VolumeSource{
@@ -1000,6 +1009,9 @@ func createOAuthContainer(name, namespace string) corev1.Container {
10001009
"--upstream-ca=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
10011010
"--email-domain=*",
10021011
"--skip-provider-button",
1012+
`--client-id=` + name + `-` + namespace + `-oauth-client`,
1013+
"--client-secret-file=/etc/oauth/client/secret",
1014+
"--scope=user:info user:check-access",
10031015
`--openshift-sar={"verb":"get","resource":"notebooks","resourceAPIGroup":"kubeflow.org",` +
10041016
`"resourceName":"` + name + `","namespace":"$(NAMESPACE)"}`,
10051017
"--logout-url=https://example.notebook-url/notebook/" + namespace + "/" + name,
@@ -1048,6 +1060,10 @@ func createOAuthContainer(name, namespace string) corev1.Container {
10481060
},
10491061
},
10501062
VolumeMounts: []corev1.VolumeMount{
1063+
{
1064+
Name: "oauth-client",
1065+
MountPath: "/etc/oauth/client",
1066+
},
10511067
{
10521068
Name: "oauth-config",
10531069
MountPath: "/etc/oauth/config",

components/odh-notebook-controller/controllers/notebook_oauth.go

Lines changed: 159 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,23 @@ import (
1919
"context"
2020
"crypto/rand"
2121
"encoding/base64"
22+
"encoding/json"
23+
"fmt"
24+
"math/big"
2225
"reflect"
2326

2427
"k8s.io/apimachinery/pkg/util/intstr"
2528

2629
nbv1 "github.com/kubeflow/kubeflow/components/notebook-controller/api/v1"
30+
oauthv1 "github.com/openshift/api/oauth/v1"
2731
routev1 "github.com/openshift/api/route/v1"
2832
corev1 "k8s.io/api/core/v1"
2933
apierrs "k8s.io/apimachinery/pkg/api/errors"
3034
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3135
"k8s.io/apimachinery/pkg/types"
3236
ctrl "sigs.k8s.io/controller-runtime"
37+
"sigs.k8s.io/controller-runtime/pkg/client"
38+
logf "sigs.k8s.io/controller-runtime/pkg/log"
3339
)
3440

3541
const (
@@ -39,12 +45,24 @@ const (
3945
// taken from https://catalog.redhat.com/software/containers/openshift4/ose-oauth-proxy/5cdb2133bed8bd5717d5ae64?image=66cefc14401df6ff4664ec43&architecture=amd64&container-tabs=overview
4046
// and kept in sync with the manifests here and in ClusterServiceVersion metadata of opendatahub operator
4147
OAuthProxyImage = "registry.redhat.io/openshift4/ose-oauth-proxy@sha256:4f8d66597feeb32bb18699326029f9a71a5aca4a57679d636b876377c2e95695"
48+
49+
// Strings used in secret generation
50+
letterRunes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
51+
52+
// Complexity of generated secrets, this will be stored in a const for now, but in the future
53+
// there is the possibility of creating a specific file to manage secrets, change the size of
54+
// the complexity if required, etc. For now, a complexity of 16 will suffice.
55+
SECRET_DEFAULT_COMPLEXITY = 16
4256
)
4357

4458
type OAuthConfig struct {
4559
ProxyImage string
4660
}
4761

62+
type OAuthClientConfig struct {
63+
Name string
64+
}
65+
4866
// NewNotebookServiceAccount defines the desired service account object
4967
func NewNotebookServiceAccount(notebook *nbv1.Notebook) *corev1.ServiceAccount {
5068
return &corev1.ServiceAccount{
@@ -209,39 +227,106 @@ func NewNotebookOAuthSecret(notebook *nbv1.Notebook) *corev1.Secret {
209227
}
210228
}
211229

230+
// The NewNotebookOAuthClientSecret will generate a random secret that will be used
231+
// by the OAuth proxy sidecar container to automatically accept the required permissions
232+
// for the user to access a notebook instead of showing up a page where the user needs to
233+
// aggree with content he might not fully understand
234+
// More info: https://issues.redhat.com/browse/RHOAIENG-11155
235+
func NewNotebookOAuthClientSecret(notebook *nbv1.Notebook) *corev1.Secret {
236+
// Generate the client secret for the OAuth proxy
237+
randomValue := make([]byte, SECRET_DEFAULT_COMPLEXITY)
238+
for i := 0; i < SECRET_DEFAULT_COMPLEXITY; i++ {
239+
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letterRunes))))
240+
if err != nil {
241+
fmt.Printf("Error generating secret: %v\n", err)
242+
}
243+
randomValue[i] = letterRunes[num.Int64()]
244+
}
245+
246+
// Create a Kubernetes secret to store the cookie secret
247+
return &corev1.Secret{
248+
ObjectMeta: metav1.ObjectMeta{
249+
Name: notebook.Name + "-oauth-client",
250+
Namespace: notebook.Namespace,
251+
Labels: map[string]string{
252+
"notebook-name": notebook.Name,
253+
},
254+
},
255+
StringData: map[string]string{
256+
"secret": string(randomValue),
257+
},
258+
}
259+
}
260+
212261
// ReconcileOAuthSecret will manage the OAuth secret reconciliation required by
213262
// the notebook OAuth proxy
214263
func (r *OpenshiftNotebookReconciler) ReconcileOAuthSecret(notebook *nbv1.Notebook, ctx context.Context) error {
215-
// Initialize logger format
216-
log := r.Log.WithValues("notebook", notebook.Name, "namespace", notebook.Namespace)
217-
218264
// Generate the desired OAuth secret
219-
desiredSecret := NewNotebookOAuthSecret(notebook)
265+
desiredOAuthSecret := NewNotebookOAuthSecret(notebook)
266+
_ = r.createSecret(notebook, ctx, desiredOAuthSecret)
267+
268+
// Generate the desired OAuthClientSecret
269+
desiredOAuthClientSecret := NewNotebookOAuthClientSecret(notebook)
270+
_ = r.createSecret(notebook, ctx, desiredOAuthClientSecret)
271+
272+
return nil
273+
}
274+
275+
func (r *OpenshiftNotebookReconciler) ReconcileOAuthClient(notebook *nbv1.Notebook, ctx context.Context) error {
276+
log := logf.FromContext(ctx)
220277

221-
// Create the OAuth secret if it does not already exist
222-
foundSecret := &corev1.Secret{}
278+
route := &routev1.Route{}
279+
err := r.Get(ctx, types.NamespacedName{
280+
Name: notebook.Name,
281+
Namespace: notebook.Namespace,
282+
}, route)
283+
if err != nil {
284+
if apierrs.IsNotFound(err) {
285+
log.Info("Route not found, cannot create OAuthClient yet", "route", notebook.Name)
286+
return nil
287+
}
288+
log.Error(err, "Failed to get Route for OAuthClient")
289+
return err
290+
}
291+
292+
err = r.createOAuthClient(notebook, ctx)
293+
if err != nil {
294+
log.Error(err, "Unable to handle OAuthClient creation / update")
295+
return err
296+
}
297+
298+
return nil
299+
}
300+
301+
func (r *OpenshiftNotebookReconciler) createSecret(notebook *nbv1.Notebook, ctx context.Context, desiredSecret *corev1.Secret) error {
302+
log := logf.FromContext(ctx)
303+
304+
secret := &corev1.Secret{}
223305
err := r.Get(ctx, types.NamespacedName{
224306
Name: desiredSecret.Name,
225307
Namespace: notebook.Namespace,
226-
}, foundSecret)
308+
}, secret)
309+
227310
if err != nil {
228311
if apierrs.IsNotFound(err) {
229-
log.Info("Creating OAuth Secret")
230-
// Add .metatada.ownerReferences to the OAuth secret to be deleted by
312+
log.Info("Creating ", "secret", desiredSecret.Name)
313+
314+
// Add .metatada.ownerReferences to the OAuth client secret to be deleted by
231315
// the Kubernetes garbage collector if the notebook is deleted
232316
err = ctrl.SetControllerReference(notebook, desiredSecret, r.Scheme)
233317
if err != nil {
234-
log.Error(err, "Unable to add OwnerReference to the OAuth Secret")
318+
log.Error(err, "Unable to add OwnerReference to secret ", desiredSecret.Name)
235319
return err
236320
}
237-
// Create the OAuth secret in the Openshift cluster
321+
322+
// Create the OAuth client secret in the Openshift cluster
238323
err = r.Create(ctx, desiredSecret)
239324
if err != nil && !apierrs.IsAlreadyExists(err) {
240-
log.Error(err, "Unable to create the OAuth Secret")
325+
log.Error(err, "Unable to create secret ", desiredSecret.Name)
241326
return err
242327
}
243328
} else {
244-
log.Error(err, "Unable to fetch the OAuth Secret")
329+
log.Error(err, "Unable to fetch secret")
245330
return err
246331
}
247332
}
@@ -264,3 +349,64 @@ func (r *OpenshiftNotebookReconciler) ReconcileOAuthRoute(
264349
notebook *nbv1.Notebook, ctx context.Context) error {
265350
return r.reconcileRoute(notebook, ctx, NewNotebookOAuthRoute)
266351
}
352+
353+
func (r *OpenshiftNotebookReconciler) createOAuthClient(notebook *nbv1.Notebook, ctx context.Context) error {
354+
log := logf.FromContext(ctx)
355+
356+
// Get the route that will be used in the OAuthClient
357+
oauthClientRoute := &routev1.Route{}
358+
err := r.Get(ctx, types.NamespacedName{
359+
Name: notebook.Name,
360+
Namespace: notebook.Namespace,
361+
}, oauthClientRoute)
362+
if err != nil {
363+
log.Error(err, "Unable to retrieve route", "route", notebook.Name)
364+
return err
365+
}
366+
367+
// Get the secret that will be used in the OAuthClient
368+
secret := &corev1.Secret{}
369+
err = r.Get(ctx, types.NamespacedName{
370+
Name: notebook.Name + "-oauth-client",
371+
Namespace: notebook.Namespace,
372+
}, secret)
373+
if err != nil {
374+
log.Error(err, "Unable to retrieve secret", "secret", notebook.Name)
375+
return err
376+
}
377+
378+
stringData := string(secret.Data["secret"])
379+
380+
oauthClient := &oauthv1.OAuthClient{
381+
TypeMeta: metav1.TypeMeta{
382+
Kind: "OAuthClient",
383+
APIVersion: "oauth.openshift.io/v1",
384+
},
385+
ObjectMeta: metav1.ObjectMeta{
386+
Name: notebook.Name + "-" + notebook.Namespace + "-oauth-client",
387+
Labels: map[string]string{
388+
"notebook-owner": notebook.Name,
389+
},
390+
},
391+
Secret: stringData,
392+
RedirectURIs: []string{"https://" + oauthClientRoute.Spec.Host},
393+
GrantMethod: oauthv1.GrantHandlerAuto,
394+
}
395+
396+
err = r.Create(ctx, oauthClient)
397+
if err != nil {
398+
if apierrs.IsAlreadyExists(err) {
399+
data, err := json.Marshal(oauthClient)
400+
if err != nil {
401+
return fmt.Errorf("failed to create OAuth Client: %w", err)
402+
}
403+
if err = r.Patch(ctx, oauthClient, client.RawPatch(types.ApplyPatchType, data),
404+
client.ForceOwnership, client.FieldOwner("odh-notebook-controller")); err != nil {
405+
return fmt.Errorf("failed to patch existing OAuthClient CR: %w", err)
406+
}
407+
return nil
408+
}
409+
}
410+
411+
return nil
412+
}

components/odh-notebook-controller/controllers/notebook_webhook.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ func InjectOAuthProxy(notebook *nbv1.Notebook, oauth OAuthConfig) error {
120120
"--upstream-ca=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
121121
"--email-domain=*",
122122
"--skip-provider-button",
123+
`--client-id=` + notebook.Name + `-` + notebook.Namespace + `-oauth-client`,
124+
"--client-secret-file=/etc/oauth/client/secret",
125+
"--scope=user:info user:check-access",
123126
`--openshift-sar={"verb":"get","resource":"notebooks","resourceAPIGroup":"kubeflow.org",` +
124127
`"resourceName":"` + notebook.Name + `","namespace":"$(NAMESPACE)"}`,
125128
},
@@ -167,6 +170,10 @@ func InjectOAuthProxy(notebook *nbv1.Notebook, oauth OAuthConfig) error {
167170
},
168171
},
169172
VolumeMounts: []corev1.VolumeMount{
173+
{
174+
Name: "oauth-client",
175+
MountPath: "/etc/oauth/client",
176+
},
170177
{
171178
Name: "oauth-config",
172179
MountPath: "/etc/oauth/config",
@@ -222,6 +229,29 @@ func InjectOAuthProxy(notebook *nbv1.Notebook, oauth OAuthConfig) error {
222229
*notebookVolumes = append(*notebookVolumes, oauthVolume)
223230
}
224231

232+
// Add the OAuth Client configuration volume:
233+
// https://pkg.go.dev/k8s.io/api/core/v1#Volume
234+
clientVolumeExists := false
235+
clientVolume := corev1.Volume{
236+
Name: "oauth-client",
237+
VolumeSource: corev1.VolumeSource{
238+
Secret: &corev1.SecretVolumeSource{
239+
SecretName: notebook.Name + "-oauth-client",
240+
DefaultMode: ptr.To[int32](420),
241+
},
242+
},
243+
}
244+
for index, volume := range *notebookVolumes {
245+
if volume.Name == "oauth-client" {
246+
(*notebookVolumes)[index] = clientVolume
247+
clientVolumeExists = true
248+
break
249+
}
250+
}
251+
if !clientVolumeExists {
252+
*notebookVolumes = append(*notebookVolumes, clientVolume)
253+
}
254+
225255
// Add the TLS certificates volume:
226256
// https://pkg.go.dev/k8s.io/api/core/v1#Volume
227257
tlsVolumeExists := false

components/odh-notebook-controller/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040

4141
nbv1 "github.com/kubeflow/kubeflow/components/notebook-controller/api/v1"
4242
configv1 "github.com/openshift/api/config/v1"
43+
oauthv1 "github.com/openshift/api/oauth/v1"
4344
routev1 "github.com/openshift/api/route/v1"
4445
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
4546
//+kubebuilder:scaffold:imports
@@ -56,6 +57,7 @@ func init() {
5657
utilruntime.Must(nbv1.AddToScheme(scheme))
5758
utilruntime.Must(routev1.AddToScheme(scheme))
5859
utilruntime.Must(configv1.AddToScheme(scheme))
60+
utilruntime.Must(oauthv1.AddToScheme(scheme))
5961

6062
//+kubebuilder:scaffold:scheme
6163
}

0 commit comments

Comments
 (0)