diff --git a/frontend/src/__mocks__/mockRoleBindingK8sResource.ts b/frontend/src/__mocks__/mockRoleBindingK8sResource.ts index b67c63a4a7..4fec421779 100644 --- a/frontend/src/__mocks__/mockRoleBindingK8sResource.ts +++ b/frontend/src/__mocks__/mockRoleBindingK8sResource.ts @@ -1,6 +1,5 @@ import { genUID } from '~/__mocks__/mockUtils'; -import { KnownLabels, RoleBindingKind } from '~/k8sTypes'; -import { RoleBindingSubject } from '~/types'; +import { KnownLabels, RoleBindingKind, RoleBindingSubject } from '~/k8sTypes'; type MockResourceConfigType = { name?: string; @@ -8,6 +7,7 @@ type MockResourceConfigType = { subjects?: RoleBindingSubject[]; roleRefName?: string; uid?: string; + modelRegistryName?: string; }; export const mockRoleBindingK8sResource = ({ @@ -22,22 +22,38 @@ export const mockRoleBindingK8sResource = ({ ], roleRefName = 'view', uid = genUID('rolebinding'), -}: MockResourceConfigType): RoleBindingKind => ({ - kind: 'RoleBinding', - apiVersion: 'rbac.authorization.k8s.io/v1', - metadata: { - name, - namespace, - uid, - creationTimestamp: '2023-02-14T21:43:59Z', - labels: { + modelRegistryName = '', +}: MockResourceConfigType): RoleBindingKind => { + let labels; + if (modelRegistryName) { + labels = { + 'app.kubernetes.io/name': modelRegistryName, + app: modelRegistryName, + 'app.kubernetes.io/component': 'model-registry', + 'app.kubernetes.io/part-of': 'model-registry', [KnownLabels.DASHBOARD_RESOURCE]: 'true', + component: 'model-registry', + }; + } else { + labels = { + [KnownLabels.DASHBOARD_RESOURCE]: 'true', + }; + } + return { + kind: 'RoleBinding', + apiVersion: 'rbac.authorization.k8s.io/v1', + metadata: { + name, + namespace, + uid, + creationTimestamp: '2023-02-14T21:43:59Z', + labels, + }, + subjects, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: modelRegistryName ? 'Role' : 'ClusterRole', + name: roleRefName, }, - }, - subjects, - roleRef: { - apiGroup: 'rbac.authorization.k8s.io', - kind: 'ClusterRole', - name: roleRefName, - }, -}); + }; +}; diff --git a/frontend/src/__tests__/cypress/cypress/pages/components/table.ts b/frontend/src/__tests__/cypress/cypress/pages/components/table.ts index 4fbc7d0aa1..7915e8d252 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/components/table.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/components/table.ts @@ -19,4 +19,8 @@ export class TableRow extends Contextual { findKebabAction(name: string): Cypress.Chainable> { return this.find().findKebabAction(name); } + + findKebab(): Cypress.Chainable> { + return this.find().findKebab(); + } } diff --git a/frontend/src/__tests__/cypress/cypress/pages/modelRegistryPermissions.ts b/frontend/src/__tests__/cypress/cypress/pages/modelRegistryPermissions.ts new file mode 100644 index 0000000000..c93356ce2e --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/pages/modelRegistryPermissions.ts @@ -0,0 +1,72 @@ +import { Contextual } from './components/Contextual'; +import { TableRow } from './components/table'; + +class PermissionsTableRow extends TableRow {} + +class UsersTab { + visit(mrName: string, wait = true) { + cy.visitWithLogin(`/modelRegistrySettings/permissions/${mrName}`); + if (wait) { + this.wait(); + } + } + + private wait() { + cy.findByTestId('app-page-title'); + cy.testA11y(); + } + + findAddUserButton() { + return cy.findByTestId('add-button User'); + } + + findAddGroupButton() { + return cy.findByTestId('add-button Group'); + } + + getUserTable() { + return new PermissionTable(() => cy.findByTestId('role-binding-table User')); + } + + getGroupTable() { + return new PermissionTable(() => cy.findByTestId('role-binding-table Group')); + } +} + +class PermissionTable extends Contextual { + findRows() { + return this.find().find(`[data-label=Username]`); + } + + findAddInput() { + return this.find().findByTestId('role-binding-name-input'); + } + + findEditInput(id: string) { + return this.find().findByTestId(['role-binding-name-input', id]); + } + + findGroupSelect() { + return this.find().get(`[aria-label="Name selection"]`); + } + + getTableRow(name: string) { + return new PermissionsTableRow(() => + this.find().find(`[data-label=Username]`).contains(name).parents('tr'), + ); + } + + findTableHeaderButton(name: string) { + return this.find().find('thead').findByRole('button', { name }); + } + + findSaveNewButton() { + return this.find().findByTestId(['save-new-button']); + } + + findEditSaveButton(id: string) { + return this.find().findByTestId(['save-button', id]); + } +} + +export const usersTab = new UsersTab(); diff --git a/frontend/src/__tests__/cypress/cypress/pages/permissions.ts b/frontend/src/__tests__/cypress/cypress/pages/permissions.ts index b52cea50fe..c77aede5ff 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/permissions.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/permissions.ts @@ -23,11 +23,11 @@ class PermissionsTab { } getUserTable() { - return new PermissionTable(() => cy.findByTestId('project-sharing-table User')); + return new PermissionTable(() => cy.findByTestId('role-binding-table User')); } getGroupTable() { - return new PermissionTable(() => cy.findByTestId('project-sharing-table Group')); + return new PermissionTable(() => cy.findByTestId('role-binding-table Group')); } } @@ -37,11 +37,11 @@ class PermissionTable extends Contextual { } findAddInput() { - return this.find().findByTestId('project-sharing-name-input'); + return this.find().findByTestId('role-binding-name-input'); } findEditInput(id: string) { - return this.find().findByTestId(['project-sharing-name-input', id]); + return this.find().findByTestId(['role-binding-name-input', id]); } getTableRow(name: string) { @@ -56,7 +56,7 @@ class PermissionTable extends Contextual { selectPermission(id: string, name: string) { return this.find() - .findByTestId(['project-sharing-name-input', id]) + .findByTestId(['role-binding-name-input', id]) .parents('tr') .findByRole('button', { name: 'Options menu' }) .findSelectOption(name) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts new file mode 100644 index 0000000000..885f41d2a9 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts @@ -0,0 +1,384 @@ +import { mockK8sResourceList } from '~/__mocks__'; +import { mock200Status } from '~/__mocks__/mockK8sStatus'; +import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResource'; +import { be } from '~/__tests__/cypress/cypress/utils/should'; +import { + GroupModel, + ModelRegistryModel, + RoleBindingModel, +} from '~/__tests__/cypress/cypress/utils/models'; +import type { RoleBindingSubject } from '~/k8sTypes'; +import { asProductAdminUser, asProjectEditUser } from '~/__tests__/cypress/cypress/utils/users'; +import { mockModelRegistry } from '~/__mocks__/mockModelRegistry'; +import { mockGroup } from '~/__mocks__/mockGroup'; +import { usersTab } from '~/__tests__/cypress/cypress/pages/modelRegistryPermissions'; + +const MODEL_REGISTRY_DEFAULT_NAMESPACE = 'odh-model-registries'; + +const userSubjects: RoleBindingSubject[] = [ + { + kind: 'User', + apiGroup: 'rbac.authorization.k8s.io', + name: 'example-mr-user', + }, +]; + +const groupSubjects: RoleBindingSubject[] = [ + { + kind: 'Group', + apiGroup: 'rbac.authorization.k8s.io', + name: 'example-mr-users', + }, +]; + +type HandlersProps = { + isEmpty?: boolean; + hasPermission?: boolean; +}; + +const initIntercepts = ({ isEmpty = false, hasPermission = true }: HandlersProps) => { + if (!hasPermission) { + asProjectEditUser(); + } else { + asProductAdminUser(); + } + cy.interceptK8sList( + ModelRegistryModel, + mockK8sResourceList([ + mockModelRegistry({ name: 'example-mr', namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE }), + ]), + ); + cy.interceptK8sList( + { model: GroupModel }, + mockK8sResourceList([mockGroup({ name: 'example-mr-group-option' })]), + ); + cy.interceptK8sList( + { model: RoleBindingModel, ns: MODEL_REGISTRY_DEFAULT_NAMESPACE }, + mockK8sResourceList( + isEmpty + ? [] + : [ + mockRoleBindingK8sResource({ + namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE, + name: 'example-mr-user', + subjects: userSubjects, + roleRefName: 'registry-user-example-mr', + modelRegistryName: 'example-mr', + }), + mockRoleBindingK8sResource({ + namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE, + name: 'example-mr-user-2', + subjects: [{ ...userSubjects[0], name: 'example-mr-user-2' }], + roleRefName: 'registry-user-example-mr', + modelRegistryName: 'example-mr', + }), + mockRoleBindingK8sResource({ + namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE, + name: 'example-mr-users', + subjects: groupSubjects, + roleRefName: 'registry-user-example-mr', + modelRegistryName: 'example-mr', + }), + mockRoleBindingK8sResource({ + namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE, + name: 'example-mr-users-2', + subjects: [{ ...groupSubjects[0], name: 'example-mr-users-2' }], + roleRefName: 'registry-user-example-mr', + modelRegistryName: 'example-mr', + }), + ], + ), + ); +}; + +describe('MR Permissions', () => { + const userTable = usersTab.getUserTable(); + const groupTable = usersTab.getGroupTable(); + + it('should not be accessible for non-project admins', () => { + initIntercepts({ isEmpty: false, hasPermission: false }); + usersTab.visit('example-mr', false); + cy.findByTestId('not-found-page').should('exist'); + }); + + it('redirect if no modelregistry', () => { + initIntercepts({ isEmpty: true }); + usersTab.visit('example-mr'); + cy.url().should('eq', `${Cypress.config().baseUrl}/modelRegistrySettings`); + }); + + describe('Users table', () => { + it('Table sorting for users table', () => { + initIntercepts({ isEmpty: false }); + usersTab.visit('example-mr'); + userTable.findRows().should('have.length', 2); + + // by name + userTable.findTableHeaderButton('Name').click(); + userTable.findTableHeaderButton('Name').should(be.sortDescending); + userTable.findTableHeaderButton('Name').click(); + userTable.findTableHeaderButton('Name').should(be.sortAscending); + + //by date added + userTable.findTableHeaderButton('Date added').click(); + userTable.findTableHeaderButton('Date added').should(be.sortAscending); + userTable.findTableHeaderButton('Date added').click(); + userTable.findTableHeaderButton('Date added').should(be.sortDescending); + }); + + it('Add user', () => { + initIntercepts({ isEmpty: false }); + cy.interceptK8s( + 'POST', + RoleBindingModel, + mockRoleBindingK8sResource({ + namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE, + name: 'new-example-mr-user', + subjects: userSubjects, + roleRefName: 'registry-user-example-mr', + modelRegistryName: 'example-mr', + }), + ).as('addUser'); + usersTab.visit('example-mr'); + + usersTab.findAddUserButton().click(); + + userTable.findAddInput().fill('new-example-mr-user'); + userTable.findSaveNewButton().click(); + + cy.wait('@addUser').then((interception) => { + expect(interception.request.body).to.containSubset({ + metadata: { + labels: { + app: 'example-mr', + 'app.kubernetes.io/component': 'model-registry', + 'app.kubernetes.io/part-of': 'model-registry', + 'app.kubernetes.io/name': 'example-mr', + 'opendatahub.io/dashboard': 'true', + component: 'model-registry', + }, + }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'Role', + name: 'registry-user-example-mr', + }, + subjects: [ + { apiGroup: 'rbac.authorization.k8s.io', kind: 'User', name: 'new-example-mr-user' }, + ], + }); + }); + }); + + it('Edit user', () => { + initIntercepts({ isEmpty: false }); + cy.interceptK8s( + 'POST', + RoleBindingModel, + mockRoleBindingK8sResource({ + namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE, + name: 'edited-user', + subjects: [{ ...userSubjects[0], name: 'edited-user' }], + roleRefName: 'registry-user-example-mr', + modelRegistryName: 'example-mr', + }), + ).as('editUser'); + cy.interceptK8s( + 'DELETE', + { model: RoleBindingModel, ns: MODEL_REGISTRY_DEFAULT_NAMESPACE, name: 'example-mr-user' }, + mock200Status({}), + ).as('deleteUser'); + + usersTab.visit('example-mr'); + + userTable.getTableRow('example-mr-user').findKebabAction('Edit').click(); + userTable.findEditInput('example-mr-user').clear().type('edited-user'); + userTable.findEditSaveButton('edited-user').click(); + + cy.wait('@editUser').then((interception) => { + expect(interception.request.body).to.containSubset({ + metadata: { + labels: { + app: 'example-mr', + 'app.kubernetes.io/component': 'model-registry', + 'app.kubernetes.io/part-of': 'model-registry', + 'app.kubernetes.io/name': 'example-mr', + 'opendatahub.io/dashboard': 'true', + component: 'model-registry', + }, + }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'Role', + name: 'registry-user-example-mr', + }, + subjects: [{ apiGroup: 'rbac.authorization.k8s.io', kind: 'User', name: 'edited-user' }], + }); + }); + cy.wait('@deleteUser'); + }); + + it('Delete user', () => { + initIntercepts({ isEmpty: false }); + + cy.interceptK8s( + 'DELETE', + { model: RoleBindingModel, ns: MODEL_REGISTRY_DEFAULT_NAMESPACE, name: 'example-mr-user' }, + mock200Status({}), + ).as('deleteUser'); + usersTab.visit('example-mr'); + + userTable.getTableRow('example-mr-user').findKebabAction('Delete').click(); + + cy.wait('@deleteUser'); + }); + }); + + describe('Groups table', () => { + it('Table sorting for groups table', () => { + initIntercepts({ isEmpty: false }); + + usersTab.visit('example-mr'); + + groupTable.findTableHeaderButton('Name').click(); + groupTable.findTableHeaderButton('Name').should(be.sortDescending); + groupTable.findTableHeaderButton('Name').click(); + groupTable.findTableHeaderButton('Name').should(be.sortAscending); + + groupTable.findTableHeaderButton('Date added').click(); + groupTable.findTableHeaderButton('Date added').should(be.sortAscending); + groupTable.findTableHeaderButton('Date added').click(); + groupTable.findTableHeaderButton('Date added').should(be.sortDescending); + }); + + it('Add group', () => { + initIntercepts({ isEmpty: false }); + cy.interceptK8s( + 'POST', + RoleBindingModel, + mockRoleBindingK8sResource({ + namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE, + name: 'new-example-mr-group', + subjects: groupSubjects, + roleRefName: 'registry-user-example-mr', + modelRegistryName: 'example-mr', + }), + ).as('addGroup'); + usersTab.visit('example-mr'); + + usersTab.findAddGroupButton().click(); + + groupTable.findGroupSelect().fill('new-example-mr-group'); + cy.findByText('Create "new-example-mr-group"').click(); + groupTable.findSaveNewButton().click(); + + cy.wait('@addGroup').then((interception) => { + expect(interception.request.body).to.containSubset({ + metadata: { + labels: { + app: 'example-mr', + 'app.kubernetes.io/component': 'model-registry', + 'app.kubernetes.io/part-of': 'model-registry', + 'app.kubernetes.io/name': 'example-mr', + 'opendatahub.io/dashboard': 'true', + component: 'model-registry', + }, + }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'Role', + name: 'registry-user-example-mr', + }, + subjects: [ + { apiGroup: 'rbac.authorization.k8s.io', kind: 'Group', name: 'new-example-mr-group' }, + ], + }); + }); + }); + + it('Edit group', () => { + initIntercepts({ isEmpty: false }); + cy.interceptK8s( + 'POST', + RoleBindingModel, + mockRoleBindingK8sResource({ + namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE, + name: 'example-mr-group-option', + subjects: [{ ...groupSubjects[0], name: 'example-mr-group-option' }], + roleRefName: 'registry-user-example-mr', + modelRegistryName: 'example-mr', + }), + ).as('editGroup'); + cy.interceptK8s( + 'DELETE', + { + model: RoleBindingModel, + ns: MODEL_REGISTRY_DEFAULT_NAMESPACE, + name: 'example-mr-users-2', + }, + mock200Status({}), + ).as('deleteGroup'); + + usersTab.visit('example-mr'); + + groupTable.getTableRow('example-mr-users-2').findKebabAction('Edit').click(); + groupTable.findGroupSelect().clear().type('example-mr-group-opti'); + cy.findByText('example-mr-group-option').click(); + groupTable.findEditSaveButton('example-mr-group-option').click(); + + cy.wait('@editGroup').then((interception) => { + expect(interception.request.body).to.containSubset({ + metadata: { + labels: { + 'opendatahub.io/dashboard': 'true', + app: 'example-mr', + 'app.kubernetes.io/component': 'model-registry', + 'app.kubernetes.io/part-of': 'model-registry', + 'app.kubernetes.io/name': 'example-mr', + component: 'model-registry', + }, + }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'Role', + name: 'registry-user-example-mr', + }, + subjects: [ + { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'Group', + name: 'example-mr-group-option', + }, + ], + }); + }); + cy.wait('@deleteGroup'); + }); + + it('Delete group', () => { + initIntercepts({ isEmpty: false }); + + cy.interceptK8s( + 'DELETE', + { + model: RoleBindingModel, + ns: MODEL_REGISTRY_DEFAULT_NAMESPACE, + name: 'example-mr-users-2', + }, + mock200Status({}), + ).as('deleteGroup'); + + usersTab.visit('example-mr'); + groupTable.getTableRow('example-mr-users-2').findKebabAction('Delete').click(); + + cy.wait('@deleteGroup'); + }); + + it('Disabled actions on default group', () => { + initIntercepts({ isEmpty: false }); + usersTab.visit('example-mr'); + groupTable.getTableRow('example-mr-users').findKebab().should('be.disabled'); + groupTable.getTableRow('example-mr-users-2').findKebab().should('not.be.disabled'); + }); + }); +}); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistrySettings.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistrySettings.cy.ts index ff49d58210..a7834edbee 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistrySettings.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistrySettings.cy.ts @@ -185,10 +185,6 @@ describe('ManagePermissionsModal', () => { .findByText('Manage permissions') .click(); }); - - it('Shows modal for registry', () => { - cy.findByText('Manage permissions of test-registry-1').should('exist'); - }); }); describe('DeleteModelRegistryModal', () => { diff --git a/frontend/src/api/k8s/__tests__/roleBindings.spec.ts b/frontend/src/api/k8s/__tests__/roleBindings.spec.ts index 7d79ff4f77..b9846938e5 100644 --- a/frontend/src/api/k8s/__tests__/roleBindings.spec.ts +++ b/frontend/src/api/k8s/__tests__/roleBindings.spec.ts @@ -10,20 +10,20 @@ import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResour import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; import { mock200Status, mock404Error } from '~/__mocks__/mockK8sStatus'; import { KnownLabels, RoleBindingKind, RoleBindingSubject } from '~/k8sTypes'; -import { - ProjectSharingRBType, - ProjectSharingRoleType, -} from '~/pages/projects/projectSharing/types'; import { createRoleBinding, deleteRoleBinding, - generateRoleBindingProjectSharing, + generateRoleBindingPermissions, generateRoleBindingServingRuntime, getRoleBinding, listRoleBindings, patchRoleBindingOwnerRef, } from '~/api/k8s/roleBindings'; import { RoleBindingModel } from '~/api/models/k8s'; +import { + RoleBindingPermissionsRBType, + RoleBindingPermissionsRoleType, +} from '~/concepts/roleBinding/types'; jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({ k8sListResource: jest.fn(), @@ -84,30 +84,31 @@ describe('generateRoleBindingServingRuntime', () => { }); }); -describe('generateRoleBindingProjectSharing', () => { +describe('generateRoleBindingPermissions', () => { it('should generate project sharing role binding when RB type is USER and role type is EDIT', () => { - const result = generateRoleBindingProjectSharing( + const result = generateRoleBindingPermissions( namespace, - ProjectSharingRBType.USER, + RoleBindingPermissionsRBType.USER, 'rbSubjectName', - ProjectSharingRoleType.EDIT, + RoleBindingPermissionsRoleType.EDIT, + 'ClusterRole', ); const subjects = [ { apiGroup: 'rbac.authorization.k8s.io', - kind: ProjectSharingRBType.USER, + kind: RoleBindingPermissionsRBType.USER, name: 'rbSubjectName', }, ]; - createRoleBindingObject(ProjectSharingRoleType.EDIT, subjects); + createRoleBindingObject(RoleBindingPermissionsRoleType.EDIT, subjects); expect(result.apiVersion).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.EDIT, subjects).apiVersion, + createRoleBindingObject(RoleBindingPermissionsRoleType.EDIT, subjects).apiVersion, ); expect(result.subjects).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.EDIT, subjects).subjects, + createRoleBindingObject(RoleBindingPermissionsRoleType.EDIT, subjects).subjects, ); expect(result.roleRef).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.EDIT, subjects).roleRef, + createRoleBindingObject(RoleBindingPermissionsRoleType.EDIT, subjects).roleRef, ); expect(result.metadata.name).toMatch(/^dashboard-permissions-[a-zA-Z0-9]+$/); expect(result.metadata.labels).toStrictEqual({ @@ -117,28 +118,29 @@ describe('generateRoleBindingProjectSharing', () => { }); it('should generate project sharing role binding when RB type is USER and role type is ADMIN', () => { - const result = generateRoleBindingProjectSharing( + const result = generateRoleBindingPermissions( namespace, - ProjectSharingRBType.USER, + RoleBindingPermissionsRBType.USER, 'rbSubjectName', - ProjectSharingRoleType.ADMIN, + RoleBindingPermissionsRoleType.ADMIN, + 'ClusterRole', ); const subjects = [ { apiGroup: 'rbac.authorization.k8s.io', - kind: ProjectSharingRBType.USER, + kind: RoleBindingPermissionsRBType.USER, name: 'rbSubjectName', }, ]; - createRoleBindingObject(ProjectSharingRoleType.ADMIN, subjects); + createRoleBindingObject(RoleBindingPermissionsRoleType.ADMIN, subjects); expect(result.apiVersion).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.ADMIN, subjects).apiVersion, + createRoleBindingObject(RoleBindingPermissionsRoleType.ADMIN, subjects).apiVersion, ); expect(result.subjects).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.ADMIN, subjects).subjects, + createRoleBindingObject(RoleBindingPermissionsRoleType.ADMIN, subjects).subjects, ); expect(result.roleRef).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.ADMIN, subjects).roleRef, + createRoleBindingObject(RoleBindingPermissionsRoleType.ADMIN, subjects).roleRef, ); expect(result.metadata.name).toMatch(/^dashboard-permissions-[a-zA-Z0-9]+$/); expect(result.metadata.labels).toStrictEqual({ @@ -148,28 +150,29 @@ describe('generateRoleBindingProjectSharing', () => { }); it('should generate project sharing role binding when RB type is GROUP and role type is EDIT', () => { - const result = generateRoleBindingProjectSharing( + const result = generateRoleBindingPermissions( namespace, - ProjectSharingRBType.GROUP, + RoleBindingPermissionsRBType.GROUP, 'rbSubjectName', - ProjectSharingRoleType.EDIT, + RoleBindingPermissionsRoleType.EDIT, + 'ClusterRole', ); const subjects = [ { apiGroup: 'rbac.authorization.k8s.io', - kind: ProjectSharingRBType.GROUP, + kind: RoleBindingPermissionsRBType.GROUP, name: 'rbSubjectName', }, ]; - createRoleBindingObject(ProjectSharingRoleType.EDIT, subjects); + createRoleBindingObject(RoleBindingPermissionsRoleType.EDIT, subjects); expect(result.apiVersion).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.EDIT, subjects).apiVersion, + createRoleBindingObject(RoleBindingPermissionsRoleType.EDIT, subjects).apiVersion, ); expect(result.subjects).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.EDIT, subjects).subjects, + createRoleBindingObject(RoleBindingPermissionsRoleType.EDIT, subjects).subjects, ); expect(result.roleRef).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.EDIT, subjects).roleRef, + createRoleBindingObject(RoleBindingPermissionsRoleType.EDIT, subjects).roleRef, ); expect(result.metadata.name).toMatch(/^dashboard-permissions-[a-zA-Z0-9]+$/); expect(result.metadata.labels).toStrictEqual({ @@ -179,28 +182,29 @@ describe('generateRoleBindingProjectSharing', () => { }); it('should generate project sharing role binding when RB type is GROUP and role type is ADMIN', () => { - const result = generateRoleBindingProjectSharing( + const result = generateRoleBindingPermissions( namespace, - ProjectSharingRBType.GROUP, + RoleBindingPermissionsRBType.GROUP, 'rbSubjectName', - ProjectSharingRoleType.ADMIN, + RoleBindingPermissionsRoleType.ADMIN, + 'ClusterRole', ); const subjects = [ { apiGroup: 'rbac.authorization.k8s.io', - kind: ProjectSharingRBType.GROUP, + kind: RoleBindingPermissionsRBType.GROUP, name: 'rbSubjectName', }, ]; - createRoleBindingObject(ProjectSharingRoleType.ADMIN, subjects); + createRoleBindingObject(RoleBindingPermissionsRoleType.ADMIN, subjects); expect(result.apiVersion).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.ADMIN, subjects).apiVersion, + createRoleBindingObject(RoleBindingPermissionsRoleType.ADMIN, subjects).apiVersion, ); expect(result.subjects).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.ADMIN, subjects).subjects, + createRoleBindingObject(RoleBindingPermissionsRoleType.ADMIN, subjects).subjects, ); expect(result.roleRef).toStrictEqual( - createRoleBindingObject(ProjectSharingRoleType.ADMIN, subjects).roleRef, + createRoleBindingObject(RoleBindingPermissionsRoleType.ADMIN, subjects).roleRef, ); expect(result.metadata.name).toMatch(/^dashboard-permissions-[a-zA-Z0-9]+$/); expect(result.metadata.labels).toStrictEqual({ diff --git a/frontend/src/api/k8s/roleBindings.ts b/frontend/src/api/k8s/roleBindings.ts index d1ff629b08..7805523b5b 100644 --- a/frontend/src/api/k8s/roleBindings.ts +++ b/frontend/src/api/k8s/roleBindings.ts @@ -6,15 +6,20 @@ import { k8sListResource, k8sPatchResource, K8sStatus, + K8sResourceCommon, } from '@openshift/dynamic-plugin-sdk-utils'; -import { K8sAPIOptions, KnownLabels, RoleBindingKind } from '~/k8sTypes'; -import { RoleBindingModel } from '~/api/models'; import { - ProjectSharingRBType, - ProjectSharingRoleType, -} from '~/pages/projects/projectSharing/types'; + K8sAPIOptions, + KnownLabels, + RoleBindingKind, + RoleBindingRoleRef, + RoleBindingSubject, +} from '~/k8sTypes'; +import { RoleBindingModel } from '~/api/models'; import { genRandomChars } from '~/utilities/string'; import { applyK8sAPIOptions } from '~/api/apiMergeUtils'; +import { RoleBindingPermissionsRoleType } from '~/concepts/roleBinding/types'; +import { addOwnerReference } from '~/api/k8sUtils'; export const generateRoleBindingServingRuntime = ( name: string, @@ -46,11 +51,17 @@ export const generateRoleBindingServingRuntime = ( return roleBindingObject; }; -export const generateRoleBindingProjectSharing = ( +export const generateRoleBindingPermissions = ( namespace: string, - rbSubjectType: ProjectSharingRBType, - rbSubjectName: string, - rbRoleRefType: ProjectSharingRoleType, + rbSubjectKind: RoleBindingSubject['kind'], + rbSubjectName: RoleBindingSubject['name'], + rbRoleRefName: RoleBindingPermissionsRoleType | string, //string because with MR this can include MR name + rbRoleRefKind: RoleBindingRoleRef['kind'], + rbLabels: { [key: string]: string } = { + [KnownLabels.DASHBOARD_RESOURCE]: 'true', + [KnownLabels.PROJECT_SHARING]: 'true', + }, + ownerReference?: K8sResourceCommon, ): RoleBindingKind => { const roleBindingObject: RoleBindingKind = { apiVersion: 'rbac.authorization.k8s.io/v1', @@ -58,25 +69,22 @@ export const generateRoleBindingProjectSharing = ( metadata: { name: `dashboard-permissions-${genRandomChars()}`, namespace, - labels: { - [KnownLabels.DASHBOARD_RESOURCE]: 'true', - [KnownLabels.PROJECT_SHARING]: 'true', - }, + labels: rbLabels, }, roleRef: { apiGroup: 'rbac.authorization.k8s.io', - kind: 'ClusterRole', - name: rbRoleRefType, + kind: rbRoleRefKind, + name: rbRoleRefName, }, subjects: [ { apiGroup: 'rbac.authorization.k8s.io', - kind: rbSubjectType, + kind: rbSubjectKind, name: rbSubjectName, }, ], }; - return roleBindingObject; + return addOwnerReference(roleBindingObject, ownerReference); }; export const listRoleBindings = ( diff --git a/frontend/src/app/AppRoutes.tsx b/frontend/src/app/AppRoutes.tsx index 468b0fb358..cc6ee4516d 100644 --- a/frontend/src/app/AppRoutes.tsx +++ b/frontend/src/app/AppRoutes.tsx @@ -13,6 +13,7 @@ import { import { useCheckJupyterEnabled } from '~/utilities/notebookControllerUtils'; import { SupportedArea } from '~/concepts/areas'; import useIsAreaAvailable from '~/concepts/areas/useIsAreaAvailable'; +import ModelRegistrySettingsRoutes from '~/pages/modelRegistrySettings/ModelRegistrySettingsRoutes'; const HomePage = React.lazy(() => import('../pages/home/Home')); @@ -49,9 +50,6 @@ const ClusterSettingsPage = React.lazy(() => import('../pages/clusterSettings/Cl const CustomServingRuntimeRoutes = React.lazy( () => import('../pages/modelServing/customServingRuntimes/CustomServingRuntimeRoutes'), ); -const ModelRegistrySettingsPage = React.lazy( - () => import('../pages/modelRegistrySettings/ModelRegistrySettings'), -); const GroupSettingsPage = React.lazy(() => import('../pages/groupSettings/GroupSettings')); const LearningCenterPage = React.lazy(() => import('../pages/learningCenter/LearningCenter')); const BYONImagesPage = React.lazy(() => import('../pages/BYONImages/BYONImages')); @@ -125,7 +123,7 @@ const AppRoutes: React.FC = () => { } /> } /> } /> - } /> + } /> } /> )} diff --git a/frontend/src/concepts/roleBinding/RoleBindingPermissions.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissions.tsx new file mode 100644 index 0000000000..105f7d98d1 --- /dev/null +++ b/frontend/src/concepts/roleBinding/RoleBindingPermissions.tsx @@ -0,0 +1,139 @@ +import * as React from 'react'; +import { + EmptyState, + EmptyStateBody, + EmptyStateIcon, + EmptyStateVariant, + PageSection, + Spinner, + Stack, + StackItem, + EmptyStateHeader, +} from '@patternfly/react-core'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { K8sResourceCommon } from '@openshift/dynamic-plugin-sdk-utils'; +import { GroupKind, RoleBindingKind, RoleBindingRoleRef } from '~/k8sTypes'; +import { ProjectSectionID } from '~/pages/projects/screens/detail/types'; +import { ContextResourceData } from '~/types'; +import RoleBindingPermissionsTableSection from './RoleBindingPermissionsTableSection'; +import { RoleBindingPermissionsRBType, RoleBindingPermissionsRoleType } from './types'; +import { filterRoleBindingSubjects } from './utils'; + +type RoleBindingPermissionsProps = { + ownerReference?: K8sResourceCommon; + roleBindingPermissionsRB: ContextResourceData; + defaultRoleBindingName?: string; + permissionOptions: { + type: RoleBindingPermissionsRoleType; + description: string; + }[]; + projectName: string; + roleRefKind: RoleBindingRoleRef['kind']; + roleRefName?: RoleBindingRoleRef['name']; + labels?: { [key: string]: string }; + description: React.ReactElement | string; + groups: GroupKind[]; + isGroupFirst?: boolean; +}; + +const RoleBindingPermissions: React.FC = ({ + ownerReference, + roleBindingPermissionsRB, + defaultRoleBindingName, + permissionOptions, + projectName, + roleRefKind, + roleRefName, + labels, + description, + groups, + isGroupFirst = false, +}) => { + const { + data: roleBindings, + loaded, + error: loadError, + refresh: refreshRB, + } = roleBindingPermissionsRB; + if (loadError) { + return ( + + } + headingLevel="h2" + /> + {loadError.message} + + ); + } + + if (!loaded) { + return ( + + + + + ); + } + + const userTable = ( + + ); + + const groupTable = ( + 0 ? groups.map((group: GroupKind) => group.metadata.name) : undefined + } + typeModifier="group" + /> + ); + + return ( + + + {description} + {isGroupFirst ? groupTable : userTable} + {isGroupFirst ? userTable : groupTable} + + + ); +}; + +export default RoleBindingPermissions; diff --git a/frontend/src/pages/projects/projectSharing/ProjectSharingNameInput.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissionsNameInput.tsx similarity index 68% rename from frontend/src/pages/projects/projectSharing/ProjectSharingNameInput.tsx rename to frontend/src/concepts/roleBinding/RoleBindingPermissionsNameInput.tsx index 0c6c6d1fe5..3be122a651 100644 --- a/frontend/src/pages/projects/projectSharing/ProjectSharingNameInput.tsx +++ b/frontend/src/concepts/roleBinding/RoleBindingPermissionsNameInput.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { TextInput } from '@patternfly/react-core'; import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated'; -import { ProjectSharingRBType } from '~/pages/projects/projectSharing/types'; +import { RoleBindingSubject } from '~/k8sTypes'; +import { RoleBindingPermissionsRBType } from './types'; -type ProjectSharingNameInputProps = { - type: ProjectSharingRBType; +type RoleBindingPermissionsNameInputProps = { + subjectKind: RoleBindingSubject['kind']; value: string; onChange: (selection: string) => void; onClear: () => void; @@ -12,8 +13,8 @@ type ProjectSharingNameInputProps = { typeAhead?: string[]; }; -const ProjectSharingNameInput: React.FC = ({ - type, +const RoleBindingPermissionsNameInput: React.FC = ({ + subjectKind, value, onChange, onClear, @@ -25,12 +26,14 @@ const ProjectSharingNameInput: React.FC = ({ if (!typeAhead) { return ( onChange(newValue)} /> ); @@ -63,4 +66,4 @@ const ProjectSharingNameInput: React.FC = ({ ); }; -export default ProjectSharingNameInput; +export default RoleBindingPermissionsNameInput; diff --git a/frontend/src/concepts/roleBinding/RoleBindingPermissionsPermissionSelection.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissionsPermissionSelection.tsx new file mode 100644 index 0000000000..e6941e6a0a --- /dev/null +++ b/frontend/src/concepts/roleBinding/RoleBindingPermissionsPermissionSelection.tsx @@ -0,0 +1,43 @@ +import { Select, SelectOption } from '@patternfly/react-core/deprecated'; +import React from 'react'; +import { RoleBindingPermissionsRoleType } from './types'; +import { castRoleBindingPermissionsRoleType, roleLabel } from './utils'; + +type RoleBindingPermissionsPermissionSelectionProps = { + selection: string; + permissionOptions: { + type: RoleBindingPermissionsRoleType; + description: string; + }[]; + onSelect: (roleType: RoleBindingPermissionsRoleType) => void; +}; + +const RoleBindingPermissionsPermissionSelection: React.FC< + RoleBindingPermissionsPermissionSelectionProps +> = ({ selection, onSelect, permissionOptions }) => { + const [isOpen, setIsOpen] = React.useState(false); + + return ( + + ); +}; + +export default RoleBindingPermissionsPermissionSelection; diff --git a/frontend/src/concepts/roleBinding/RoleBindingPermissionsTable.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTable.tsx new file mode 100644 index 0000000000..a40c8d06aa --- /dev/null +++ b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTable.tsx @@ -0,0 +1,134 @@ +import * as React from 'react'; +import { K8sResourceCommon } from '@openshift/dynamic-plugin-sdk-utils'; +import { Table } from '~/components/table'; +import { RoleBindingKind, RoleBindingRoleRef, RoleBindingSubject } from '~/k8sTypes'; +import { deleteRoleBinding, generateRoleBindingPermissions, createRoleBinding } from '~/api'; +import RoleBindingPermissionsTableRow from './RoleBindingPermissionsTableRow'; +import { columnsRoleBindingPermissions } from './data'; +import { RoleBindingPermissionsRoleType } from './types'; +import { firstSubject } from './utils'; +import RoleBindingPermissionsTableRowAdd from './RoleBindingPermissionsTableRowAdd'; + +type RoleBindingPermissionsTableProps = { + ownerReference?: K8sResourceCommon; + subjectKind: RoleBindingSubject['kind']; + namespace: string; + roleRefKind: RoleBindingRoleRef['kind']; + roleRefName?: RoleBindingRoleRef['name']; + labels?: { [key: string]: string }; + defaultRoleBindingName?: string; + permissions: RoleBindingKind[]; + permissionOptions: { + type: RoleBindingPermissionsRoleType; + description: string; + }[]; + isAdding: boolean; + typeAhead?: string[]; + onDismissNewRow: () => void; + onError: (error: Error) => void; + refresh: () => void; +}; + +const RoleBindingPermissionsTable: React.FC = ({ + ownerReference, + subjectKind, + namespace, + roleRefKind, + roleRefName, + labels, + defaultRoleBindingName, + permissions, + permissionOptions, + typeAhead, + isAdding, + onDismissNewRow, + onError, + refresh, +}) => { + const [editCell, setEditCell] = React.useState([]); + return ( + + isAdding ? ( + { + const newRBObject = generateRoleBindingPermissions( + namespace, + subjectKind, + subjectName, + roleRefName || rbRoleRefName, + roleRefKind, + labels, + ownerReference, + ); + createRoleBinding(newRBObject) + .then(() => { + onDismissNewRow(); + refresh(); + }) + .catch((e) => { + onError(e); + }); + }} + onCancel={onDismissNewRow} + /> + ) : null + } + rowRenderer={(rb) => ( + { + const newRBObject = generateRoleBindingPermissions( + namespace, + subjectKind, + subjectName, + roleRefName || rbRoleRefName, + roleRefKind, + labels, + ownerReference, + ); + createRoleBinding(newRBObject) + .then(() => + deleteRoleBinding(rb.metadata.name, rb.metadata.namespace) + .then(() => refresh()) + .catch((e) => { + onError(e); + setEditCell((prev) => prev.filter((cell) => cell !== rb.metadata.name)); + }), + ) + .catch((e) => { + onError(e); + setEditCell((prev) => prev.filter((cell) => cell !== rb.metadata.name)); + }); + refresh(); + }} + onDelete={() => { + deleteRoleBinding(rb.metadata.name, rb.metadata.namespace).then(() => refresh()); + }} + onEdit={() => { + setEditCell((prev) => [...prev, rb.metadata.name]); + }} + onCancel={() => { + setEditCell((prev) => prev.filter((cell) => cell !== rb.metadata.name)); + }} + /> + )} + /> + ); +}; +export default RoleBindingPermissionsTable; diff --git a/frontend/src/pages/projects/projectSharing/ProjectSharingTableRow.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx similarity index 62% rename from frontend/src/pages/projects/projectSharing/ProjectSharingTableRow.tsx rename to frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx index e000cba1a0..94f1515c07 100644 --- a/frontend/src/pages/projects/projectSharing/ProjectSharingTableRow.tsx +++ b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx @@ -2,38 +2,48 @@ import * as React from 'react'; import { ActionsColumn, Tbody, Td, Tr } from '@patternfly/react-table'; import { Button, + Icon, Split, SplitItem, Text, Timestamp, TimestampTooltipVariant, + Tooltip, } from '@patternfly/react-core'; -import { CheckIcon, TimesIcon } from '@patternfly/react-icons'; -import { RoleBindingKind } from '~/k8sTypes'; +import { CheckIcon, OutlinedQuestionCircleIcon, TimesIcon } from '@patternfly/react-icons'; +import { RoleBindingKind, RoleBindingSubject } from '~/k8sTypes'; import { relativeTime } from '~/utilities/time'; -import { castProjectSharingRoleType, firstSubject, roleLabel } from './utils'; -import { ProjectSharingRBType, ProjectSharingRoleType } from './types'; -import ProjectSharingNameInput from './ProjectSharingNameInput'; -import ProjectSharingPermissionSelection from './ProjectSharingPermissionSelection'; +import { castRoleBindingPermissionsRoleType, firstSubject, roleLabel } from './utils'; +import { RoleBindingPermissionsRoleType } from './types'; +import RoleBindingPermissionsNameInput from './RoleBindingPermissionsNameInput'; +import RoleBindingPermissionsPermissionSelection from './RoleBindingPermissionsPermissionSelection'; -type ProjectSharingTableRowProps = { - obj: RoleBindingKind; - type: ProjectSharingRBType; +type RoleBindingPermissionsTableRowProps = { + roleBindingObject: RoleBindingKind; + subjectKind: RoleBindingSubject['kind']; isEditing: boolean; + defaultRoleBindingName?: string; + permissionOptions: { + type: RoleBindingPermissionsRoleType; + description: string; + }[]; typeAhead?: string[]; - onChange: (name: string, roleType: ProjectSharingRoleType) => void; + onChange: (name: string, roleType: RoleBindingPermissionsRoleType) => void; onCancel: () => void; onEdit: () => void; onDelete: () => void; }; const defaultValueName = (obj: RoleBindingKind) => firstSubject(obj); -const defaultValueRole = (obj: RoleBindingKind) => castProjectSharingRoleType(obj.roleRef.name); +const defaultValueRole = (obj: RoleBindingKind) => + castRoleBindingPermissionsRoleType(obj.roleRef.name); -const ProjectSharingTableRow: React.FC = ({ - obj, - type, +const RoleBindingPermissionsTableRow: React.FC = ({ + roleBindingObject: obj, + subjectKind, isEditing, + defaultRoleBindingName, + permissionOptions, typeAhead, onChange, onCancel, @@ -41,19 +51,19 @@ const ProjectSharingTableRow: React.FC = ({ onDelete, }) => { const [roleBindingName, setRoleBindingName] = React.useState(defaultValueName(obj)); - const [roleBindingRoleRef, setRoleBindingRoleRef] = React.useState( - defaultValueRole(obj), - ); + const [roleBindingRoleRef, setRoleBindingRoleRef] = + React.useState(defaultValueRole(obj)); const [isLoading, setIsLoading] = React.useState(false); const createdDate = new Date(obj.metadata.creationTimestamp || ''); + const isDefaultGroup = obj.metadata.name === defaultRoleBindingName; return ( - setIsPermissionsModalOpen(false)} - refresh={refresh} - /> ( + + } /> + } /> + } /> + +); + +export default ModelRegistrySettingsRoutes; diff --git a/frontend/src/pages/modelRegistrySettings/useModelRegistryRoleBindings.ts b/frontend/src/pages/modelRegistrySettings/useModelRegistryRoleBindings.ts new file mode 100644 index 0000000000..38d1a06fca --- /dev/null +++ b/frontend/src/pages/modelRegistrySettings/useModelRegistryRoleBindings.ts @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { listRoleBindings } from '~/api'; +import { MODEL_REGISTRY_DEFAULT_NAMESPACE } from '~/concepts/modelRegistry/const'; +import { KnownLabels, RoleBindingKind } from '~/k8sTypes'; +import useFetchState, { FetchState } from '~/utilities/useFetchState'; + +const useModelRegistryRoleBindings = (): FetchState => { + const getRoleBindings = React.useCallback( + () => + listRoleBindings( + MODEL_REGISTRY_DEFAULT_NAMESPACE, + KnownLabels.LABEL_SELECTOR_MODEL_REGISTRY, + ).catch((e) => { + if (e.statusObject?.code === 404) { + throw new Error('No rolebindings found.'); + } + throw e; + }), + [], + ); + + return useFetchState(getRoleBindings, []); +}; + +export default useModelRegistryRoleBindings; diff --git a/frontend/src/pages/projects/projectSharing/ProjectSharing.tsx b/frontend/src/pages/projects/projectSharing/ProjectSharing.tsx index 9163c11ca8..9c0e500031 100644 --- a/frontend/src/pages/projects/projectSharing/ProjectSharing.tsx +++ b/frontend/src/pages/projects/projectSharing/ProjectSharing.tsx @@ -1,89 +1,33 @@ import * as React from 'react'; -import { - EmptyState, - EmptyStateBody, - EmptyStateIcon, - EmptyStateVariant, - PageSection, - Spinner, - Stack, - StackItem, - EmptyStateHeader, -} from '@patternfly/react-core'; -import { ExclamationCircleIcon } from '@patternfly/react-icons'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; -import { GroupKind } from '~/k8sTypes'; -import { ProjectSectionID } from '~/pages/projects/screens/detail/types'; -import ProjectSharingTableSection from './ProjectSharingTableSection'; -import { ProjectSharingRBType } from './types'; -import { filterRoleBindingSubjects } from './utils'; +import RoleBindingPermissions from '~/concepts/roleBinding/RoleBindingPermissions'; +import { RoleBindingPermissionsRoleType } from '~/concepts/roleBinding/types'; const ProjectSharing: React.FC = () => { const { - projectSharingRB: { data: roleBindings, loaded, error: loadError, refresh: refreshRB }, + currentProject, + projectSharingRB, groups: [groups], } = React.useContext(ProjectDetailsContext); - if (loadError) { - return ( - - } - headingLevel="h2" - /> - {loadError.message} - - ); - } - - if (!loaded) { - return ( - - - - - ); - } - return ( - - - Add users and groups that can access the project. - - - - - 0 ? groups.map((group: GroupKind) => group.metadata.name) : undefined - } - typeModifier="group" - /> - - - + ); }; diff --git a/frontend/src/pages/projects/projectSharing/ProjectSharingPermissionSelection.tsx b/frontend/src/pages/projects/projectSharing/ProjectSharingPermissionSelection.tsx deleted file mode 100644 index 8934096811..0000000000 --- a/frontend/src/pages/projects/projectSharing/ProjectSharingPermissionSelection.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Select, SelectOption } from '@patternfly/react-core/deprecated'; -import React from 'react'; -import { ProjectSharingRoleType } from './types'; -import { castProjectSharingRoleType, roleLabel } from './utils'; - -type ProjectSharingPermissionSelectionProps = { - selection: string; - onSelect: (roleType: ProjectSharingRoleType) => void; -}; - -const ProjectSharingPermissions = [ - { - type: ProjectSharingRoleType.EDIT, - description: 'View and edit the project components', - }, - { - type: ProjectSharingRoleType.ADMIN, - description: 'Edit the project and manage user access', - }, -]; - -const ProjectSharingPermissionSelection: React.FC = ({ - selection, - onSelect, -}) => { - const [isOpen, setIsOpen] = React.useState(false); - - return ( - - ); -}; - -export default ProjectSharingPermissionSelection; diff --git a/frontend/src/pages/projects/projectSharing/ProjectSharingTable.tsx b/frontend/src/pages/projects/projectSharing/ProjectSharingTable.tsx deleted file mode 100644 index e68f33dcd2..0000000000 --- a/frontend/src/pages/projects/projectSharing/ProjectSharingTable.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import * as React from 'react'; -import { Table } from '~/components/table'; -import { RoleBindingKind } from '~/k8sTypes'; -import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; -import { deleteRoleBinding, generateRoleBindingProjectSharing, createRoleBinding } from '~/api'; -import ProjectSharingTableRow from './ProjectSharingTableRow'; -import { columnsProjectSharing } from './data'; -import { ProjectSharingRBType } from './types'; -import { firstSubject } from './utils'; -import ProjectSharingTableRowAdd from './ProjectSharingTableRowAdd'; - -type ProjectSharingTableProps = { - type: ProjectSharingRBType; - permissions: RoleBindingKind[]; - isAdding: boolean; - typeAhead?: string[]; - onDismissNewRow: () => void; - onError: (error: Error) => void; - refresh: () => void; -}; - -const ProjectSharingTable: React.FC = ({ - type, - permissions, - typeAhead, - isAdding, - onDismissNewRow, - onError, - refresh, -}) => { - const { currentProject } = React.useContext(ProjectDetailsContext); - - const [editCell, setEditCell] = React.useState([]); - - return ( -
{isEditing ? ( - { setRoleBindingName(selection); @@ -63,12 +73,29 @@ const ProjectSharingTableRow: React.FC = ({ typeAhead={typeAhead} /> ) : ( - {roleBindingName} + + {roleBindingName} + {` `} + {isDefaultGroup && ( + + This group is created by default. You can add users to this group via the API. + + } + > + + + + + )} + )} - {isEditing ? ( - 1 ? ( + { setRoleBindingRoleRef(selection); @@ -123,6 +150,7 @@ const ProjectSharingTableRow: React.FC = ({ ) : ( = ({ ); }; -export default ProjectSharingTableRow; +export default RoleBindingPermissionsTableRow; diff --git a/frontend/src/pages/projects/projectSharing/ProjectSharingTableRowAdd.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRowAdd.tsx similarity index 55% rename from frontend/src/pages/projects/projectSharing/ProjectSharingTableRowAdd.tsx rename to frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRowAdd.tsx index 88a6d16bef..2268022dc8 100644 --- a/frontend/src/pages/projects/projectSharing/ProjectSharingTableRowAdd.tsx +++ b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRowAdd.tsx @@ -1,37 +1,43 @@ import * as React from 'react'; import { Tbody, Td, Tr } from '@patternfly/react-table'; -import { Button, Split, SplitItem } from '@patternfly/react-core'; +import { Button, Split, SplitItem, Text } from '@patternfly/react-core'; import { CheckIcon, TimesIcon } from '@patternfly/react-icons'; -import { ProjectSharingRBType, ProjectSharingRoleType } from './types'; -import ProjectSharingNameInput from './ProjectSharingNameInput'; -import ProjectSharingPermissionSelection from './ProjectSharingPermissionSelection'; +import { RoleBindingSubject } from '~/k8sTypes'; +import { RoleBindingPermissionsRoleType } from './types'; +import RoleBindingPermissionsNameInput from './RoleBindingPermissionsNameInput'; +import RoleBindingPermissionsPermissionSelection from './RoleBindingPermissionsPermissionSelection'; +import { roleLabel } from './utils'; -type ProjectSharingTableRowPropsAdd = { +type RoleBindingPermissionsTableRowPropsAdd = { typeAhead?: string[]; - type: ProjectSharingRBType; - onChange: (name: string, roleType: ProjectSharingRoleType) => void; + subjectKind: RoleBindingSubject['kind']; + permissionOptions: { + type: RoleBindingPermissionsRoleType; + description: string; + }[]; + onChange: (name: string, roleType: RoleBindingPermissionsRoleType) => void; onCancel: () => void; }; -/** @deprecated - this should use ProjectSharingTableRow */ -const ProjectSharingTableRowAdd: React.FC = ({ +/** @deprecated - this should use RoleBindingPermissionsTableRow */ +const RoleBindingPermissionsTableRowAdd: React.FC = ({ typeAhead, - type, + subjectKind, + permissionOptions, onChange, onCancel, }) => { const [roleBindingName, setRoleBindingName] = React.useState(''); - const [roleBindingRoleRef, setRoleBindingRoleRef] = React.useState( - ProjectSharingRoleType.EDIT, - ); + const [roleBindingRoleRef, setRoleBindingRoleRef] = + React.useState(permissionOptions[0]?.type); const [isLoading, setIsLoading] = React.useState(false); return (
- ) => { setRoleBindingName(selection); @@ -42,12 +48,17 @@ const ProjectSharingTableRowAdd: React.FC = ({ /> - { - setRoleBindingRoleRef(selection); - }} - /> + {permissionOptions.length > 1 ? ( + { + setRoleBindingRoleRef(selection); + }} + /> + ) : ( + {roleLabel(roleBindingRoleRef)} + )} @@ -85,4 +96,4 @@ const ProjectSharingTableRowAdd: React.FC = ({ ); }; -export default ProjectSharingTableRowAdd; +export default RoleBindingPermissionsTableRowAdd; diff --git a/frontend/src/pages/projects/projectSharing/ProjectSharingTableSection.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableSection.tsx similarity index 54% rename from frontend/src/pages/projects/projectSharing/ProjectSharingTableSection.tsx rename to frontend/src/concepts/roleBinding/RoleBindingPermissionsTableSection.tsx index d0a0f0162d..a79143ba52 100644 --- a/frontend/src/pages/projects/projectSharing/ProjectSharingTableSection.tsx +++ b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableSection.tsx @@ -10,26 +10,44 @@ import { StackItem, Title, } from '@patternfly/react-core'; -import { RoleBindingKind } from '~/k8sTypes'; +import { K8sResourceCommon } from '@openshift/dynamic-plugin-sdk-utils'; +import { RoleBindingKind, RoleBindingRoleRef, RoleBindingSubject } from '~/k8sTypes'; import HeaderIcon from '~/concepts/design/HeaderIcon'; import { ProjectObjectType } from '~/concepts/design/utils'; -import ProjectSharingTable from './ProjectSharingTable'; -import { ProjectSharingRBType } from './types'; +import RoleBindingPermissionsTable from './RoleBindingPermissionsTable'; +import { RoleBindingPermissionsRBType, RoleBindingPermissionsRoleType } from './types'; -export type ProjectSharingTableSectionAltProps = { +export type RoleBindingPermissionsTableSectionAltProps = { + ownerReference?: K8sResourceCommon; roleBindings: RoleBindingKind[]; - projectSharingTableType: ProjectSharingRBType; + projectName: string; + roleRefKind: RoleBindingRoleRef['kind']; + roleRefName?: RoleBindingRoleRef['name']; + subjectKind: RoleBindingSubject['kind']; + permissionOptions: { + type: RoleBindingPermissionsRoleType; + description: string; + }[]; typeAhead?: string[]; refresh: () => void; typeModifier?: string; + defaultRoleBindingName?: string; + labels?: { [key: string]: string }; }; -const ProjectSharingTableSection: React.FC = ({ +const RoleBindingPermissionsTableSection: React.FC = ({ + ownerReference, roleBindings, - projectSharingTableType, + projectName, + roleRefKind, + roleRefName, + subjectKind, + permissionOptions, typeAhead, refresh, typeModifier, + defaultRoleBindingName, + labels, }) => { const [addField, setAddField] = React.useState(false); const [error, setError] = React.useState(undefined); @@ -45,22 +63,29 @@ const ProjectSharingTableSection: React.FC = > - - {projectSharingTableType === ProjectSharingRBType.USER ? 'Users' : 'Groups'} + <Title id={`user-permission-${subjectKind}`} headingLevel="h2" size="xl"> + {subjectKind === RoleBindingPermissionsRBType.USER ? 'Users' : 'Groups'} - { @@ -88,7 +113,7 @@ const ProjectSharingTableSection: React.FC = )} ); }; -export default ProjectSharingTableSection; +export default RoleBindingPermissionsTableSection; diff --git a/frontend/src/pages/projects/projectSharing/data.ts b/frontend/src/concepts/roleBinding/data.ts similarity index 88% rename from frontend/src/pages/projects/projectSharing/data.ts rename to frontend/src/concepts/roleBinding/data.ts index a09532f7c9..4cf5a83059 100644 --- a/frontend/src/pages/projects/projectSharing/data.ts +++ b/frontend/src/concepts/roleBinding/data.ts @@ -2,7 +2,7 @@ import { RoleBindingKind } from '~/k8sTypes'; import { SortableData } from '~/components/table'; import { firstSubject } from './utils'; -export const columnsProjectSharing: SortableData[] = [ +export const columnsRoleBindingPermissions: SortableData[] = [ { field: 'username', label: 'Name', diff --git a/frontend/src/pages/projects/projectSharing/types.ts b/frontend/src/concepts/roleBinding/types.ts similarity index 62% rename from frontend/src/pages/projects/projectSharing/types.ts rename to frontend/src/concepts/roleBinding/types.ts index ad009193f9..db9015875d 100644 --- a/frontend/src/pages/projects/projectSharing/types.ts +++ b/frontend/src/concepts/roleBinding/types.ts @@ -1,17 +1,18 @@ import { RoleBindingSubject } from '~/k8sTypes'; -export enum ProjectSharingRBType { +export enum RoleBindingPermissionsRBType { USER = 'User', GROUP = 'Group', } -export enum ProjectSharingRoleType { +export enum RoleBindingPermissionsRoleType { EDIT = 'edit', ADMIN = 'admin', + DEFAULT = 'default', } export type RoleBindingSubjectWithRole = RoleBindingSubject & { - role: ProjectSharingRoleType; + role: RoleBindingPermissionsRoleType; roleBindingName: string; roleBindingNamespace: string; }; diff --git a/frontend/src/concepts/roleBinding/utils.ts b/frontend/src/concepts/roleBinding/utils.ts new file mode 100644 index 0000000000..8b3b949d18 --- /dev/null +++ b/frontend/src/concepts/roleBinding/utils.ts @@ -0,0 +1,30 @@ +import { capitalize } from '@patternfly/react-core'; +import { RoleBindingKind } from '~/k8sTypes'; +import { RoleBindingPermissionsRBType, RoleBindingPermissionsRoleType } from './types'; + +export const filterRoleBindingSubjects = ( + roleBindings: RoleBindingKind[], + type: RoleBindingPermissionsRBType, +): RoleBindingKind[] => roleBindings.filter((roles) => roles.subjects[0]?.kind === type); + +export const castRoleBindingPermissionsRoleType = ( + role: string, +): RoleBindingPermissionsRoleType => { + if (role === RoleBindingPermissionsRoleType.ADMIN) { + return RoleBindingPermissionsRoleType.ADMIN; + } + if (role === RoleBindingPermissionsRoleType.EDIT) { + return RoleBindingPermissionsRoleType.EDIT; + } + return RoleBindingPermissionsRoleType.DEFAULT; +}; + +export const firstSubject = (roleBinding: RoleBindingKind): string => + roleBinding.subjects[0]?.name || ''; + +export const roleLabel = (value: RoleBindingPermissionsRoleType): string => { + if (value === RoleBindingPermissionsRoleType.EDIT) { + return 'Contributor'; + } + return capitalize(value); +}; diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index 3702550dfe..58714030f4 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -22,6 +22,7 @@ export enum KnownLabels { PROJECT_SHARING = 'opendatahub.io/project-sharing', MODEL_SERVING_PROJECT = 'modelmesh-enabled', DATA_CONNECTION_AWS = 'opendatahub.io/managed', + LABEL_SELECTOR_MODEL_REGISTRY = 'component=model-registry', } export type K8sVerb = @@ -503,13 +504,19 @@ export type RoleBindingSubject = { name: string; }; +export type RoleBindingRoleRef = { + kind: 'Role' | 'ClusterRole'; + apiGroup?: string; + name: string; +}; + export type RoleBindingKind = K8sResourceCommon & { metadata: { name: string; namespace: string; }; subjects: RoleBindingSubject[]; - roleRef: RoleBindingSubject; + roleRef: RoleBindingRoleRef; }; export type RouteKind = K8sResourceCommon & { diff --git a/frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx b/frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx index 480c230c02..0929e6ae7a 100644 --- a/frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx +++ b/frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx @@ -17,7 +17,6 @@ const DeleteModelRegistryModal: React.FC = ({ onClose, refresh, }) => { - const defaultPermissionsGroup = 'TODO: group here'; // TODO should be implemented as part of https://issues.redhat.com/browse/RHOAIENG-6636 const [isSubmitting, setIsSubmitting] = React.useState(false); const [error, setError] = React.useState(); const [confirmInputValue, setConfirmInputValue] = React.useState(''); @@ -72,7 +71,7 @@ const DeleteModelRegistryModal: React.FC = ({ Only the {mr.metadata.name} itself will be removed. You'll need to manually delete all data in the connected database. Additionally, the default group{' '} - {defaultPermissionsGroup} and any permissions associated with{' '} + {`${mr.metadata.name}-users`} and any permissions associated with{' '} {mr.metadata.name} will be deleted. Any other groups and roles created by you will need to be manually deleted. diff --git a/frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx b/frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx deleted file mode 100644 index 1e81f77edf..0000000000 --- a/frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { Modal } from '@patternfly/react-core'; -import { ModelRegistryKind } from '~/k8sTypes'; -import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; - -type ManagePermissionsModalProps = { - modelRegistry: ModelRegistryKind; - isOpen: boolean; - onClose: () => void; - refresh: () => Promise; -}; - -const ManagePermissionsModal: React.FC = ({ - modelRegistry: mr, - isOpen, - onClose, - refresh, -}) => ( - { - // TODO submit changes, then... - refresh(); - }} - onCancel={onClose} - isSubmitDisabled // TODO - error={undefined} // TODO - alertTitle="Error saving permissions" - /> - } - > - TODO: This feature is not yet implemented - -); - -export default ManagePermissionsModal; diff --git a/frontend/src/pages/modelRegistrySettings/ModelRegistriesPermissions.tsx b/frontend/src/pages/modelRegistrySettings/ModelRegistriesPermissions.tsx new file mode 100644 index 0000000000..590f4cee68 --- /dev/null +++ b/frontend/src/pages/modelRegistrySettings/ModelRegistriesPermissions.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { + Breadcrumb, + BreadcrumbItem, + ClipboardCopy, + Tab, + TabContent, + TabContentBody, + Tabs, +} from '@patternfly/react-core'; +import { Link } from 'react-router-dom'; +import { Navigate, useParams } from 'react-router'; +import { KnownLabels, ModelRegistryKind, RoleBindingKind } from '~/k8sTypes'; +import { useGroups } from '~/api'; +import RoleBindingPermissions from '~/concepts/roleBinding/RoleBindingPermissions'; +import { useContextResourceData } from '~/utilities/useContextResourceData'; +import ApplicationsPage from '~/pages/ApplicationsPage'; +import { MODEL_REGISTRY_DEFAULT_NAMESPACE } from '~/concepts/modelRegistry/const'; +import { SupportedArea } from '~/concepts/areas'; +import { RoleBindingPermissionsRoleType } from '~/concepts/roleBinding/types'; +import { useModelRegistryNamespaceCR } from '~/concepts/modelRegistry/context/useModelRegistryNamespaceCR'; +import useModelRegistryRoleBindings from './useModelRegistryRoleBindings'; + +const ModelRegistriesManagePermissions: React.FC = () => { + const [activeTabKey, setActiveTabKey] = React.useState('users'); + const [ownerReference, setOwnerReference] = React.useState(); + const [groups] = useGroups(); + const roleBindings = useContextResourceData(useModelRegistryRoleBindings()); + const { mrName } = useParams(); + const state = useModelRegistryNamespaceCR(MODEL_REGISTRY_DEFAULT_NAMESPACE, mrName || ''); + const [modelRegistryCR, crLoaded] = state; + const filteredRoleBindings = roleBindings.data.filter( + (rb) => rb.metadata.labels?.['app.kubernetes.io/name'] === mrName, + ); + + React.useEffect(() => { + if (modelRegistryCR) { + setOwnerReference(modelRegistryCR); + } else { + setOwnerReference(undefined); + } + }, [modelRegistryCR]); + + if ( + (roleBindings.loaded && filteredRoleBindings.length === 0) || + (crLoaded && !modelRegistryCR) + ) { + return ; + } + + return ( + + Settings + Model registry settings} + /> + Manage Permissions + + } + loaded + empty={false} + provideChildrenPadding + > + { + setActiveTabKey(tabKey.toString()); + }} + > + + + +
+ + +
+
+ ); +}; + +export default ModelRegistriesManagePermissions; diff --git a/frontend/src/pages/modelRegistrySettings/ModelRegistriesTableRow.tsx b/frontend/src/pages/modelRegistrySettings/ModelRegistriesTableRow.tsx index efcf80da8d..d4b33c8f84 100644 --- a/frontend/src/pages/modelRegistrySettings/ModelRegistriesTableRow.tsx +++ b/frontend/src/pages/modelRegistrySettings/ModelRegistriesTableRow.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { ActionsColumn, Td, Tr } from '@patternfly/react-table'; -import { Button } from '@patternfly/react-core'; +import { Link } from 'react-router-dom'; import { ModelRegistryKind } from '~/k8sTypes'; -import ManagePermissionsModal from './ManagePermissionsModal'; import ViewDatabaseConfigModal from './ViewDatabaseConfigModal'; import DeleteModelRegistryModal from './DeleteModelRegistryModal'; @@ -15,7 +14,6 @@ const ModelRegistriesTableRow: React.FC = ({ modelRegistry: mr, refresh, }) => { - const [isPermissionsModalOpen, setIsPermissionsModalOpen] = React.useState(false); const [isDatabaseConfigModalOpen, setIsDatabaseConfigModalOpen] = React.useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false); return ( @@ -23,13 +21,12 @@ const ModelRegistriesTableRow: React.FC = ({
{mr.metadata.name} - + = ({ />
- isAdding ? ( - { - const newRBObject = generateRoleBindingProjectSharing( - currentProject.metadata.name, - type, - name, - roleType, - ); - createRoleBinding(newRBObject) - .then(() => { - onDismissNewRow(); - refresh(); - }) - .catch((e) => { - onError(e); - }); - }} - onCancel={onDismissNewRow} - /> - ) : null - } - rowRenderer={(rb) => ( - { - const newRBObject = generateRoleBindingProjectSharing( - currentProject.metadata.name, - type, - name, - roleType, - ); - createRoleBinding(newRBObject) - .then(() => - deleteRoleBinding(rb.metadata.name, rb.metadata.namespace) - .then(() => refresh()) - .catch((e) => { - onError(e); - setEditCell((prev) => prev.filter((cell) => cell !== rb.metadata.name)); - }), - ) - .catch((e) => { - onError(e); - setEditCell((prev) => prev.filter((cell) => cell !== rb.metadata.name)); - }); - refresh(); - }} - onDelete={() => { - deleteRoleBinding(rb.metadata.name, rb.metadata.namespace).then(() => refresh()); - }} - onEdit={() => { - setEditCell((prev) => [...prev, rb.metadata.name]); - }} - onCancel={() => { - setEditCell((prev) => prev.filter((cell) => cell !== rb.metadata.name)); - }} - /> - )} - /> - ); -}; -export default ProjectSharingTable; diff --git a/frontend/src/pages/projects/projectSharing/utils.ts b/frontend/src/pages/projects/projectSharing/utils.ts deleted file mode 100644 index 72effd0662..0000000000 --- a/frontend/src/pages/projects/projectSharing/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RoleBindingKind } from '~/k8sTypes'; -import { ProjectSharingRBType, ProjectSharingRoleType } from './types'; - -export const filterRoleBindingSubjects = ( - roleBindings: RoleBindingKind[], - type: ProjectSharingRBType, -): RoleBindingKind[] => roleBindings.filter((roles) => roles.subjects[0]?.kind === type); - -export const castProjectSharingRoleType = (role: string): ProjectSharingRoleType => - role === ProjectSharingRoleType.ADMIN - ? ProjectSharingRoleType.ADMIN - : ProjectSharingRoleType.EDIT; - -export const firstSubject = (roleBinding: RoleBindingKind): string => - roleBinding.subjects[0]?.name || ''; - -export const roleLabel = (value: ProjectSharingRoleType): string => - value === ProjectSharingRoleType.ADMIN ? 'Admin' : 'Contributor'; diff --git a/frontend/src/services/roleBindingService.ts b/frontend/src/services/roleBindingService.ts index b117139363..b7e4ac826c 100644 --- a/frontend/src/services/roleBindingService.ts +++ b/frontend/src/services/roleBindingService.ts @@ -1,12 +1,12 @@ import axios from '~/utilities/axios'; -import { RoleBinding } from '~/types'; +import { RoleBindingKind } from '~/k8sTypes'; -export const getRoleBinding = (projectName: string, rbName: string): Promise => { +export const getRoleBinding = (projectName: string, rbName: string): Promise => { const url = `/api/rolebindings/${projectName}/${rbName}`; return axios.get(url).then((response) => response.data); }; -export const createRoleBinding = (data: RoleBinding): Promise => { +export const createRoleBinding = (data: RoleBindingKind): Promise => { const url = `/api/rolebindings`; return axios.post(url, data).then((response) => response.data); }; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 836e512049..3dc0e9caf6 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -543,17 +543,6 @@ export type Volume = { export type VolumeMount = { mountPath: string; name: string }; -export type RoleBindingSubject = { - kind: string; - apiGroup: string; - name: string; -}; - -export type RoleBinding = { - subjects: RoleBindingSubject[]; - roleRef: RoleBindingSubject; -} & K8sResourceCommon; - export type ResourceGetter = ( projectName: string, resourceName: string, diff --git a/frontend/src/utilities/notebookControllerUtils.ts b/frontend/src/utilities/notebookControllerUtils.ts index 7c7b8b8480..0eed6f6013 100644 --- a/frontend/src/utilities/notebookControllerUtils.ts +++ b/frontend/src/utilities/notebookControllerUtils.ts @@ -11,7 +11,6 @@ import { NotebookStatus, ResourceCreator, ResourceGetter, - RoleBinding, VariableRow, } from '~/types'; import { NotebookControllerContext } from '~/pages/notebookController/NotebookControllerContext'; @@ -20,6 +19,7 @@ import { EMPTY_USER_STATE } from '~/pages/notebookController/const'; import useNamespaces from '~/pages/notebookController/useNamespaces'; import { useAppContext } from '~/app/AppContext'; import { getRoute } from '~/services/routeService'; +import { RoleBindingKind } from '~/k8sTypes'; import { useWatchNotebookEvents } from './useWatchNotebookEvents'; import { useDeepCompareMemoize } from './useDeepCompareMemoize'; @@ -155,9 +155,9 @@ export const useNotebookUserState = (): NotebookControllerUserState => { export const validateNotebookNamespaceRoleBinding = async ( notebookNamespace: string, dashboardNamespace: string, -): Promise => { +): Promise => { const roleBindingName = `${notebookNamespace}-image-pullers`; - const roleBindingObject: RoleBinding = { + const roleBindingObject: RoleBindingKind = { apiVersion: 'rbac.authorization.k8s.io/v1', kind: 'RoleBinding', metadata: { @@ -177,7 +177,7 @@ export const validateNotebookNamespaceRoleBinding = async ( }, ], }; - return verifyResource( + return verifyResource( roleBindingName, dashboardNamespace, getRoleBinding,