Skip to content

feature: introduce group-level access support in central dashboard and KFAM - #317

Open
adrianAzoitei wants to merge 15 commits into
kubeflow:mainfrom
adrianAzoitei:main
Open

feature: introduce group-level access support in central dashboard and KFAM#317
adrianAzoitei wants to merge 15 commits into
kubeflow:mainfrom
adrianAzoitei:main

Conversation

@adrianAzoitei

@adrianAzoitei adrianAzoitei commented Jun 15, 2026

Copy link
Copy Markdown

Description

closes: #308
related: kubeflow/community-distribution#2910 (comment)

Demo setup

During testing the implementation, the following setup was used:

  • kind cluster
  • oauth2-proxy + dex + keycloak for auth, where keycloak is configured with two users
    • user@example.com - cluster admin
    • user1@example.com - member of the foo group
  • 3 Profiles
    • namespace-1 and namespace-2, both owned by user@example.com
    • profile-user1 owned by user1@example.com

Adding contributors

user@example.com adding:

  1. user1@example.com as a direct contributor to namespace-1
  2. the foo group to namespace-2, indirectly granting access to user1@example.com here as well.
Screenshot 2026-06-17 at 22 00 37

RoleBindings and AuthorizationPolicies

The user cases are left untouched.

For the group cases, we see a Group RoleBinding in namespace-2.

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  annotations:
    group: foo
    role: edit
  creationTimestamp: "2026-06-17T20:00:21Z"
  name: group-foo-clusterrole-edit
  namespace: namespace-2
  resourceVersion: "683393"
  uid: edd2bec7-247e-43c3-be93-483c7e30a10e
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kubeflow-edit
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: foo

And the following AuthorizationPolicy.
NOTE: The approach to the AuthorizationPolicy is still up for debate. See istio section in #308.

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  annotations:
    group: foo
    role: edit
  creationTimestamp: "2026-06-17T20:00:21Z"
  generation: 1
  name: group-foo-clusterrole-edit
  namespace: namespace-2
  resourceVersion: "683394"
  uid: a31e841a-82f2-4ce5-b458-064d19b169a6
spec:
  rules:
  - from:
    - source:
        principals:
        - cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account
        - cluster.local/ns/kubeflow/sa/ml-pipeline-ui
    when:
    - key: request.auth.claims[groups]
      values:
      - foo

Namespace selector

The namespace selector shows a (direct) label when the user is a direct contributor - i.e. User RoleBinding - to a
Profile, and a (group) one when the user is granted access through a group. This is next to the existing (Owner) label.
Screenshot 2026-06-17 at 22 01 02

What happens when there's a tie

If user1@example.com is added both directly and through a group to the same profile, the direct membership takes precedence. In other words, the namespace selector does NOT change for user1@example.com in the following case. The centraldashboard API deduplicates namespaces returned from /env-info in this case.

See deduplicateNamespaces and the test case.
Screenshot 2026-06-17 at 22 14 46

TODO

  • extend KFAM to support Group type RoleBindings: reading, adding, deleting
  • extend KFAM to add Istio AuthorizationPolicy suitable for group support
  • extend Profile controller to add Istio RequestAuthentication in each namespace owned by a Profile
  • extend dashboard to support managing and displaying group contributors
  • extend dashboard to support kubeflow-groups header
  • extend dashboard to list all namespaces with suitable RoleBindings based on both kubeflow-userid and kubeflow-groups headers
  • show which components support groups in dashboard side-menu - now we simply get a 403 if the component (e.g. jupyter web app) gets a rejected SAR when the user isn't directly a member of a Profile.

AI Policy Disclosure

See https://www.kubeflow.org/docs/about/ai_policy/.
claude-sonnet-4.6 was used as a coding assistant, mostly on the frontend - centraldashboard - component. The model did NOT run in agentic mode, every generated snippet of code was reviewed and / or modified by the submitter.

@google-oss-prow google-oss-prow Bot added do-not-merge/work-in-progress area/dashboard area - related to central dashboard labels Jun 15, 2026
@google-oss-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign thesuperzapper for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@google-oss-prow google-oss-prow Bot added area/kfam area - releated to kubeflow access management api (kfam) size/XL labels Jun 15, 2026
@adrianAzoitei
adrianAzoitei force-pushed the main branch 2 times, most recently from 7fcb123 to 058420e Compare June 15, 2026 18:33

@christian-heusel christian-heusel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey adrian, thanks for driving this forward! 😄

I see that even though you're aware of the AI Policy your commits do not carry any Assisted-By: <...> / Co-authored-by: <...> tags, so I wanted to make sure that you're aware of these specific requirements around the Kubeflow AI Policy. 🤗 🤖

@adrianAzoitei

Copy link
Copy Markdown
Author

@christian-heusel
Disclosure: If a contribution is substantially generated or refactored by AI (e.g., Claude, Codex, Cursor), include a note in the PR description or use a footer in the commit message: Assisted-by: [Agent Name], or Co-authored-by: [Agent Name].

I went with the more convenient option of adding the disclosure in the PR description 😄 . Will amend the commits where it's relevant too.

@Ramneek-kalra

Copy link
Copy Markdown

Team, do we have some updates for this FR? We are also facing same issue and this group binding will resolve much issues.

@juliusvonkohout juliusvonkohout changed the title feat: introduce group-level access support in central dashboard and KFAM feature: introduce group-level access support in central dashboard and KFAM Aug 5, 2026
@juliusvonkohout
juliusvonkohout requested a balanced review from Copilot August 5, 2026 12:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Group lookup serialization, binding annotations, compatibility, and identity validation contain blocking defects.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Adds group-level namespace access across Central Dashboard and KFAM.

Changes:

  • Propagates group identity into namespace discovery.
  • Adds group contributor management and labels.
  • Creates group RBAC and Istio authorization resources.
File summaries
File Description
components/centraldashboard/types/index.d.ts Adds user groups.
components/centraldashboard/public/components/namespace-selector.js Labels direct/group access.
components/centraldashboard/public/components/manage-users-view-contributor.pug Adds group controls.
components/centraldashboard/public/components/manage-users-view-contributor.js Manages group contributors.
components/centraldashboard/public/components/manage-users-view-contributor_test.js Tests contributor UI.
components/centraldashboard/public/components/logout-button.js Formatting cleanup.
components/centraldashboard/app/server.ts Configures groups header.
components/centraldashboard/app/clients/profile_controller.ts Extends KFAM client.
components/centraldashboard/app/attach_user_middleware.ts Parses group identity.
components/centraldashboard/app/attach_user_middleware_test.ts Tests group parsing.
components/centraldashboard/app/api_workgroup.ts Handles group bindings and deduplication.
components/centraldashboard/app/api_workgroup_test.ts Tests group namespace access.
components/access-management/main.go Adds group configuration.
components/access-management/kfam/model_binding.go Generalizes binding subjects.
components/access-management/kfam/bindings.go Implements group RBAC and authorization.
components/access-management/kfam/bindings_test.go Tests binding filtering.
components/access-management/kfam/api_default.go Extends KFAM endpoints.
components/access-management/kfam/api_default_test.go Tests groups queries.
components/access-management/api/swagger.yaml Documents group APIs.
Review details

Suppressed comments (4)

components/access-management/kfam/api_default.go:282

  • The dashboard client serializes this parameter as group-a,group-b, but json.Unmarshal only accepts JSON such as ["group-a","group-b"]. Consequently every /env-info request for a user with at least one group receives a 401 from KFAM. The linked issue also specifies a comma-separated query parameter; parse that format here and align the Swagger/tests, or serialize JSON consistently end to end.
		err = json.Unmarshal([]byte(queries.Get("groups")), &groups)

components/centraldashboard/app/api_workgroup.ts:249

  • cType is optional, and the validation above explicitly treats an omitted value as a user, but forwarding undefined makes mapSimpleBindingToWorkgroupBinding call charAt on it and return a 500. Legacy add/remove request bodies omit cType, so preserve that path by defaulting it to user.
                kind: cType,

components/access-management/kfam/api_default.go:348

  • This checks only the header length, not that the configured prefix is actually present. A value with arbitrary leading text of the same length is accepted and stripped before the result is used by isOwnerOrAdmin; require strings.HasPrefix so malformed identity headers cannot be interpreted as another user.
	if h := header.Get(c.userIdHeader); len(h) > len(c.userIdPrefix) {

components/centraldashboard/app/api_workgroup.ts:239

  • cType comes from request JSON, so the TypeScript union does not validate it at runtime. Values outside user/group skip the email check and are converted into arbitrary RBAC subject kinds; notably cType: "User" creates a valid User while bypassing the email requirement. Reject unknown values before mapping the binding.
        if ((!cType || cType === "user") && !EMAIL_RGX.test(contributor)) {
  • Files reviewed: 18/19 changed files
  • Comments generated: 4
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread components/access-management/kfam/bindings.go Outdated
Comment thread components/access-management/kfam/model_binding.go
Comment thread components/centraldashboard/app/api_workgroup.ts Outdated
Comment thread components/access-management/kfam/api_default.go Outdated
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
Assisted-by: claude-sonnet-4.6
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
Assisted-by: claude-sonnet-4.6
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
… group tests

Assisted-by: claude-sonnet-4.6
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
…n ns

Assisted-by: claude-sonnet-4.6
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 4 comments.

Suppressed comments (5)

components/access-management/kfam/bindings.go:143

  • The annotation value must be the subject name, not its kind. As written, newly created bindings are annotated user: User or group: Group; List compares that value with the requested email/group name, so these bindings never appear in user/group-filtered reads such as /env-info.
			Annotations: map[string]string{subjectKind: binding.Subject.Kind, ROLE: binding.RoleRef.Name},

components/centraldashboard/app/api_workgroup.ts:239

  • cType remains optional for the existing contributor API, but a request that omits it passes email validation and then calls subjectKind.charAt with undefined, returning a server error. Runtime input can also supply an unsupported type and bypass user validation. Default omitted values to user and reject anything except user/group before constructing the binding.
        if ((!cType || cType === "user") && !EMAIL_RGX.test(contributor)) {

components/access-management/kfam/model_binding.go:17

  • The description says existing user cases remain untouched, but renaming the v1 wire field from user to subject breaks existing KFAM clients. Requests using the previous schema deserialize with a nil Subject and then panic when creation/deletion dereferences it, while response consumers no longer find user. Preserve a backward-compatible alias/migration path or version this API change.
	Subject *rbacv1.Subject `json:"subject,omitempty"`

components/centraldashboard/app/api_workgroup.ts:113

  • Correct the typo in the precedence documentation.
 * Most powerful role that matches takes precendece.

components/access-management/main.go:57

  • The new --groups-header option is nonfunctional: groupsHeader is passed to NewKfamClient, but that constructor neither stores nor uses it, and no KFAM handler reads a groups header. Operators changing this advertised flag will see no behavior change. Either wire it into request processing or remove the flag/parameter.
	flag.StringVar(&groupsHeader, GROUPSHEADER, "groups", "Key of request header containing groups JSON array")

Comment thread components/centraldashboard/app/clients/profile_controller.ts Outdated
Comment thread components/access-management/kfam/api_default.go
Comment thread components/centraldashboard/app/attach_user_middleware.ts Outdated
Comment thread components/centraldashboard/app/api_workgroup.ts
adrianAzoitei and others added 3 commits August 15, 2026 17:52
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
- initial assumption about how istio might be passing the header is void

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
adrianAzoitei and others added 3 commits August 15, 2026 17:57
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Adrian Azoitei <azo.adrian@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/dashboard area - related to central dashboard area/kfam area - releated to kubeflow access management api (kfam) size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Group-level access support

4 participants