Skip to content

Commit

Permalink
feat(13045): mr permissions and rolebindings (#3249)
Browse files Browse the repository at this point in the history
Signed-off-by: gitdallas <5322142+gitdallas@users.noreply.github.com>
  • Loading branch information
gitdallas committed Sep 26, 2024
1 parent 60a2d8f commit b5da010
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 72 deletions.
73 changes: 73 additions & 0 deletions backend/src/routes/api/modelRegistryRoleBindings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { FastifyReply, FastifyRequest } from 'fastify';
import { secureAdminRoute } from '../../../utils/route-security';
import { KubeFastifyInstance } from '../../../types';
import {
createModelRegistryRoleBinding,
deleteModelRegistriesRolebinding,
listModelRegistryRoleBindings,
} from './modelRegistryRolebindingsUtils';
import { V1RoleBinding } from '@kubernetes/client-node';
import { getModelRegistryNamespace } from '../modelRegistries/modelRegistryUtils';

export default async (fastify: KubeFastifyInstance): Promise<void> => {
fastify.get(
`/`,
secureAdminRoute(fastify)(async (request: FastifyRequest, reply: FastifyReply) => {
try {
const mrNamespace = getModelRegistryNamespace(fastify);
return listModelRegistryRoleBindings(fastify, mrNamespace);
} catch (e) {
fastify.log.error(
`ModelRegistry RoleBindings could not be listed, ${
e.response?.body?.message || e.message
}`,
);
reply.send(e);
}
}),
);

fastify.post(
'/',
secureAdminRoute(fastify)(
async (request: FastifyRequest<{ Body: V1RoleBinding }>, reply: FastifyReply) => {
const rbRequest = request.body;
try {
const mrNamespace = getModelRegistryNamespace(fastify);
return createModelRegistryRoleBinding(fastify, rbRequest, mrNamespace);
} catch (e) {
if (e.response?.statusCode === 409) {
fastify.log.warn(`Rolebinding already present, skipping creation.`);
return {};
}

fastify.log.error(
`rolebinding could not be created: ${e.response?.body?.message || e.message}`,
);
reply.send(new Error(e.response?.body?.message));
}
},
),
);

fastify.delete(
'/:name',
secureAdminRoute(fastify)(
async (request: FastifyRequest<{ Params: { name: string } }>, reply: FastifyReply) => {
const modelRegistryNamespace = await getModelRegistryNamespace(fastify);
const { name } = request.params;
try {
const mrNamespace = getModelRegistryNamespace(fastify);
return deleteModelRegistriesRolebinding(fastify, name, mrNamespace);
} catch (e) {
fastify.log.error(
`RoleBinding ${name} could not be deleted from ${modelRegistryNamespace}, ${
e.response?.body?.message || e.message
}`,
);
reply.send(e);
}
},
),
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { K8sStatus, KnownLabels, KubeFastifyInstance } from '../../../types';
import { V1RoleBinding } from '@kubernetes/client-node';

const MODEL_REGISTRY_ROLE_BINDING_API_GROUP = 'rbac.authorization.k8s.io';
const MODEL_REGISTRY_ROLE_BINDING_API_VERSION = 'v1';
const MODEL_REGISTRY_ROLE_BINDING_PLURAL = 'rolebindings';

export const listModelRegistryRoleBindings = async (
fastify: KubeFastifyInstance,
mrNamespace: string,
): Promise<{ items: V1RoleBinding[] }> => {
const response = await (fastify.kube.customObjectsApi.listNamespacedCustomObject(
MODEL_REGISTRY_ROLE_BINDING_API_GROUP,
MODEL_REGISTRY_ROLE_BINDING_API_VERSION,
mrNamespace,
MODEL_REGISTRY_ROLE_BINDING_PLURAL,
undefined,
undefined,
undefined,
KnownLabels.LABEL_SELECTOR_MODEL_REGISTRY,
) as Promise<{ body: { items: V1RoleBinding[] } }>);
return response.body;
};

export const createModelRegistryRoleBinding = async (
fastify: KubeFastifyInstance,
rbRequest: V1RoleBinding,
mrNamespace: string,
): Promise<V1RoleBinding> => {
const response = await (fastify.kube.customObjectsApi.createNamespacedCustomObject(
MODEL_REGISTRY_ROLE_BINDING_API_GROUP,
MODEL_REGISTRY_ROLE_BINDING_API_VERSION,
mrNamespace,
MODEL_REGISTRY_ROLE_BINDING_PLURAL,
rbRequest,
) as Promise<{ body: V1RoleBinding }>);
return response.body;
};

export const deleteModelRegistriesRolebinding = async (
fastify: KubeFastifyInstance,
roleBindingName: string,
mrNamespace: string,
): Promise<K8sStatus> => {
const response = await (fastify.kube.customObjectsApi.deleteNamespacedCustomObject(
MODEL_REGISTRY_ROLE_BINDING_API_GROUP,
MODEL_REGISTRY_ROLE_BINDING_API_VERSION,
mrNamespace,
MODEL_REGISTRY_ROLE_BINDING_PLURAL,
roleBindingName,
) as Promise<{ body: K8sStatus }>);

return response.body;
};
1 change: 1 addition & 0 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,7 @@ export enum KnownLabels {
MODEL_SERVING_PROJECT = 'modelmesh-enabled',
DATA_CONNECTION_AWS = 'opendatahub.io/managed',
CONNECTION_TYPE = 'opendatahub.io/connection-type',
LABEL_SELECTOR_MODEL_REGISTRY = 'component=model-registry',
}

type ComponentNames =
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/__tests__/cypress/cypress/support/commands/odh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,19 @@ declare global {
path: { name: string };
},
response: SuccessErrorResponse,
) => Cypress.Chainable<null>) &
((
type: 'GET /api/modelRegistryRoleBindings',
response: OdhResponse<K8sResourceListResult<RoleBindingKind>>,
) => Cypress.Chainable<null>) &
((
type: 'DELETE /api/modelRegistryRoleBindings/:name',
options: { path: { name: string } },
response: OdhResponse<SuccessErrorResponse>,
) => Cypress.Chainable<null>) &
((
type: 'POST /api/modelRegistryRoleBindings',
response: OdhResponse<RoleBindingKind>,
) => Cypress.Chainable<null>);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
GroupModel,
ModelRegistryModel,
ProjectModel,
RoleBindingModel,
} from '~/__tests__/cypress/cypress/utils/models';
import type { RoleBindingSubject } from '~/k8sTypes';
import { asProductAdminUser, asProjectEditUser } from '~/__tests__/cypress/cypress/utils/mockUsers';
Expand Down Expand Up @@ -69,8 +68,8 @@ const initIntercepts = ({ isEmpty = false, hasPermission = true }: HandlersProps
mockProjectK8sResource({ k8sName: 'project-name', displayName: 'Project' }),
]),
);
cy.interceptK8sList(
{ model: RoleBindingModel, ns: MODEL_REGISTRY_DEFAULT_NAMESPACE },
cy.interceptOdh(
'GET /api/modelRegistryRoleBindings',
mockK8sResourceList(
isEmpty
? []
Expand Down Expand Up @@ -155,9 +154,8 @@ describe('MR Permissions', () => {

it('Add user', () => {
initIntercepts({ isEmpty: false });
cy.interceptK8s(
'POST',
RoleBindingModel,
cy.interceptOdh(
'POST /api/modelRegistryRoleBindings',
mockRoleBindingK8sResource({
namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE,
name: 'new-example-mr-user',
Expand Down Expand Up @@ -199,9 +197,8 @@ describe('MR Permissions', () => {

it('Edit user', () => {
initIntercepts({ isEmpty: false });
cy.interceptK8s(
'POST',
RoleBindingModel,
cy.interceptOdh(
'POST /api/modelRegistryRoleBindings',
mockRoleBindingK8sResource({
namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE,
name: 'edited-user',
Expand All @@ -210,9 +207,9 @@ describe('MR Permissions', () => {
modelRegistryName: 'example-mr',
}),
).as('editUser');
cy.interceptK8s(
'DELETE',
{ model: RoleBindingModel, ns: MODEL_REGISTRY_DEFAULT_NAMESPACE, name: 'example-mr-user' },
cy.interceptOdh(
'DELETE /api/modelRegistryRoleBindings/:name',
{ path: { name: 'example-mr-user' } },
mock200Status({}),
).as('deleteUser');

Expand Down Expand Up @@ -248,11 +245,12 @@ describe('MR Permissions', () => {
it('Delete user', () => {
initIntercepts({ isEmpty: false });

cy.interceptK8s(
'DELETE',
{ model: RoleBindingModel, ns: MODEL_REGISTRY_DEFAULT_NAMESPACE, name: 'example-mr-user' },
cy.interceptOdh(
'DELETE /api/modelRegistryRoleBindings/:name',
{ path: { name: 'example-mr-user' } },
mock200Status({}),
).as('deleteUser');

modelRegistryPermissions.visit('example-mr');

userTable.getTableRow('example-mr-user').findKebabAction('Delete').click();
Expand Down Expand Up @@ -280,9 +278,8 @@ describe('MR Permissions', () => {

it('Add group', () => {
initIntercepts({ isEmpty: false });
cy.interceptK8s(
'POST',
RoleBindingModel,
cy.interceptOdh(
'POST /api/modelRegistryRoleBindings',
mockRoleBindingK8sResource({
namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE,
name: 'new-example-mr-group',
Expand Down Expand Up @@ -329,9 +326,8 @@ describe('MR Permissions', () => {

it('Edit group', () => {
initIntercepts({ isEmpty: false });
cy.interceptK8s(
'POST',
RoleBindingModel,
cy.interceptOdh(
'POST /api/modelRegistryRoleBindings',
mockRoleBindingK8sResource({
namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE,
name: 'example-mr-group-option',
Expand All @@ -340,13 +336,9 @@ describe('MR Permissions', () => {
modelRegistryName: 'example-mr',
}),
).as('editGroup');
cy.interceptK8s(
'DELETE',
{
model: RoleBindingModel,
ns: MODEL_REGISTRY_DEFAULT_NAMESPACE,
name: 'example-mr-users-2',
},
cy.interceptOdh(
'DELETE /api/modelRegistryRoleBindings/:name',
{ path: { name: 'example-mr-users-2' } },
mock200Status({}),
).as('deleteGroup');

Expand Down Expand Up @@ -389,13 +381,9 @@ describe('MR Permissions', () => {
it('Delete group', () => {
initIntercepts({ isEmpty: false });

cy.interceptK8s(
'DELETE',
{
model: RoleBindingModel,
ns: MODEL_REGISTRY_DEFAULT_NAMESPACE,
name: 'example-mr-users-2',
},
cy.interceptOdh(
'DELETE /api/modelRegistryRoleBindings/:name',
{ path: { name: 'example-mr-users-2' } },
mock200Status({}),
).as('deleteGroup');

Expand Down Expand Up @@ -439,9 +427,8 @@ describe('MR Permissions', () => {
});

it('Add project', () => {
cy.interceptK8s(
'POST',
RoleBindingModel,
cy.interceptOdh(
'POST /api/modelRegistryRoleBindings',
mockRoleBindingK8sResource({
namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE,
subjects: projectSubjects,
Expand Down Expand Up @@ -476,23 +463,18 @@ describe('MR Permissions', () => {
});

it('Edit project', () => {
cy.interceptK8s(
'POST',
RoleBindingModel,
cy.interceptOdh(
'POST /api/modelRegistryRoleBindings',
mockRoleBindingK8sResource({
namespace: MODEL_REGISTRY_DEFAULT_NAMESPACE,
subjects: projectSubjects,
roleRefName: 'registry-user-example-mr',
modelRegistryName: 'example-mr',
}),
).as('editProject');
cy.interceptK8s(
'DELETE',
{
model: RoleBindingModel,
ns: MODEL_REGISTRY_DEFAULT_NAMESPACE,
name: 'test-name-view',
},
cy.interceptOdh(
'DELETE /api/modelRegistryRoleBindings/:name',
{ path: { name: 'test-name-view' } },
mock200Status({}),
).as('deleteProject');

Expand Down Expand Up @@ -523,18 +505,12 @@ describe('MR Permissions', () => {
});

it('Delete project', () => {
cy.interceptK8s(
'DELETE',
{
model: RoleBindingModel,
ns: MODEL_REGISTRY_DEFAULT_NAMESPACE,
name: 'test-name-view',
},
cy.interceptOdh(
'DELETE /api/modelRegistryRoleBindings/:name',
{ path: { name: 'test-name-view' } },
mock200Status({}),
).as('deleteProject');

projectTable.getTableRow('Test Project').findKebabAction('Delete').click();

cy.wait('@deleteProject');
});
});
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/concepts/roleBinding/RoleBindingPermissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
EmptyStateHeader,
} from '@patternfly/react-core';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import { K8sResourceCommon } from '@openshift/dynamic-plugin-sdk-utils';
import { K8sResourceCommon, K8sStatus } from '@openshift/dynamic-plugin-sdk-utils';
import { GroupKind, RoleBindingKind, RoleBindingRoleRef } from '~/k8sTypes';
import { ProjectSectionID } from '~/pages/projects/screens/detail/types';
import { ContextResourceData } from '~/types';
Expand All @@ -27,6 +27,8 @@ type RoleBindingPermissionsProps = {
type: RoleBindingPermissionsRoleType;
description: string;
}[];
createRoleBinding: (roleBinding: RoleBindingKind) => Promise<RoleBindingKind>;
deleteRoleBinding: (name: string, namespace: string) => Promise<K8sStatus>;
projectName: string;
roleRefKind: RoleBindingRoleRef['kind'];
roleRefName?: RoleBindingRoleRef['name'];
Expand All @@ -42,6 +44,8 @@ const RoleBindingPermissions: React.FC<RoleBindingPermissionsProps> = ({
defaultRoleBindingName,
permissionOptions,
projectName,
createRoleBinding,
deleteRoleBinding,
roleRefKind,
roleRefName,
labels,
Expand Down Expand Up @@ -98,6 +102,8 @@ const RoleBindingPermissions: React.FC<RoleBindingPermissionsProps> = ({
subjectKind={RoleBindingPermissionsRBType.USER}
refresh={refreshRB}
typeModifier="user"
createRoleBinding={createRoleBinding}
deleteRoleBinding={deleteRoleBinding}
/>
);

Expand All @@ -117,6 +123,8 @@ const RoleBindingPermissions: React.FC<RoleBindingPermissionsProps> = ({
groups.length > 0 ? groups.map((group: GroupKind) => group.metadata.name) : undefined
}
typeModifier="group"
createRoleBinding={createRoleBinding}
deleteRoleBinding={deleteRoleBinding}
/>
);

Expand Down
Loading

0 comments on commit b5da010

Please sign in to comment.