diff --git a/cypress/e2e/blueprints/explorer/rbac/third-party-principals-get.ts b/cypress/e2e/blueprints/explorer/rbac/third-party-principals-get.ts new file mode 100644 index 00000000000..c93b684b10d --- /dev/null +++ b/cypress/e2e/blueprints/explorer/rbac/third-party-principals-get.ts @@ -0,0 +1,42 @@ +const res = { + type: 'collection', + links: { self: 'https://localhost:8005/v3/principals' }, + actions: { search: 'https://localhost:8005/v3/principals?action=search' }, + pagination: { limit: 1000 }, + sort: { + order: 'asc', + reverse: 'https://localhost:8005/v3/principals?order=desc', + links: { + loginName: 'https://localhost:8005/v3/principals?sort=loginName', name: 'https://localhost:8005/v3/principals?sort=name', principalType: 'https://localhost:8005/v3/principals?sort=principalType', profilePicture: 'https://localhost:8005/v3/principals?sort=profilePicture', profileURL: 'https://localhost:8005/v3/principals?sort=profileURL', provider: 'https://localhost:8005/v3/principals?sort=provider', uuid: 'https://localhost:8005/v3/principals?sort=uuid' + } + }, + filters: { + created: null, creatorId: null, id: null, loginName: null, me: null, memberOf: null, name: null, principalType: null, profilePicture: null, profileURL: null, provider: null, removed: null, uuid: null + }, + resourceType: 'principal', + data: [ + { + baseType: 'principal', + created: null, + creatorId: null, + id: 'github://1234567890', + links: { self: 'https://localhost:8005/v3/principals/github:%2F%2F1234567890' }, + loginName: 'admin', + me: true, + memberOf: false, + name: 'Default Admin', + principalType: 'user', + provider: 'github', + type: 'principal' + } + ] +}; + +export function spoofThirdPartyPrincipal(): Cypress.Chainable { + return cy.intercept('GET', '/v3/principals', (req) => { + req.reply({ + statusCode: 200, + body: res + }); + }).as('spoofThirdPartyPrincipal'); +} diff --git a/cypress/e2e/tests/pages/explorer2/project-namespace.spec.ts b/cypress/e2e/tests/pages/explorer2/project-namespace.spec.ts index 8780ac92664..3d7c8f8253e 100644 --- a/cypress/e2e/tests/pages/explorer2/project-namespace.spec.ts +++ b/cypress/e2e/tests/pages/explorer2/project-namespace.spec.ts @@ -1,4 +1,5 @@ import ProjectsNamespacesPagePo from '@/cypress/e2e/po/pages/explorer/projects-namespaces.po'; +import { spoofThirdPartyPrincipal } from '@/cypress/e2e/blueprints/explorer/rbac/third-party-principals-get'; describe('Projects/Namespaces', { tags: ['@explorer2', '@adminUser'] }, () => { const projectsNamespacesPage = new ProjectsNamespacesPagePo('local'); @@ -19,10 +20,53 @@ describe('Projects/Namespaces', { tags: ['@explorer2', '@adminUser'] }, () => { projectsNamespacesPage.nsProject().checkExists(); }); - describe('Create Project validation', () => { + describe('Project creation', () => { + beforeEach(() => { + cy.createE2EResourceName('proj').as('projectName'); + cy.intercept('POST', '/v3/projects').as('createProjectRequest'); + }); + + it('sets the creator principal id annotation when creating a project and using third-party auth', () => { + cy.get('@projectName').then((projectName) => { + // intercept the request to /v3/principals and return a principal authenticated by github instead of local + spoofThirdPartyPrincipal(); + + projectsNamespacesPage.createProjectButtonClick(); + projectsNamespacesPage.name().set(projectName); + projectsNamespacesPage.buttonSubmit().click(); + + cy.wait('@createProjectRequest').then(({ request, response }) => { + expect(request.body.annotations['field.cattle.io/creator-principal-name']).to.equal('github://1234567890'); + expect(response.body.annotations['field.cattle.io/creator-principal-name']).to.equal('github://1234567890'); + }); + }); + }); + + it('does not set a creator principal id annotation when creating a project if using local auth', () => { + cy.get('@projectName').then((projectName) => { + projectsNamespacesPage.createProjectButtonClick(); + projectsNamespacesPage.name().set(projectName); + projectsNamespacesPage.buttonSubmit().click(); + + cy.wait('@createProjectRequest').then(({ request, response }) => { + expect(request.body.annotations).to.not.have.property('field.cattle.io/creator-principal-name'); + expect(response.body.annotations).to.not.have.property('field.cattle.io/creator-principal-name'); + }); + }); + }); + + afterEach(() => { + cy.get('@projectName').then((projectName) => { + cy.deleteRancherResource('v3', 'projects', projectName, false); + }); + }); + }); + + describe('Project Error Banner and Validation', () => { beforeEach(() => { projectsNamespacesPage.goTo(); }); + // Issue 5975: create button should be disabled unless name is filled in it('Create button becomes available if the name is filled in', () => { projectsNamespacesPage.createProjectButtonClick(); @@ -30,12 +74,6 @@ describe('Projects/Namespaces', { tags: ['@explorer2', '@adminUser'] }, () => { projectsNamespacesPage.name().set('test-1234'); projectsNamespacesPage.buttonSubmit().expectToBeEnabled(); }); - }); - - describe('Create Project Error Banner', () => { - beforeEach(() => { - projectsNamespacesPage.goTo(); - }); it('displays an error message when submitting a form with errors', () => { projectsNamespacesPage.createProjectButtonClick(); diff --git a/pkg/aks/components/CruAks.vue b/pkg/aks/components/CruAks.vue index 1248529071e..5253fd7d5bb 100644 --- a/pkg/aks/components/CruAks.vue +++ b/pkg/aks/components/CruAks.vue @@ -55,6 +55,7 @@ import { nodePoolNamesUnique, nodePoolCount } from '../util/validators'; +import { CREATOR_PRINCIPAL_ID } from '@shell/config/labels-annotations'; export const defaultNodePool = { availabilityZones: ['1', '2', '3'], @@ -95,6 +96,7 @@ const defaultCluster = { enableClusterMonitoring: false, enableNetworkPolicy: false, labels: {}, + annotations: {}, windowsPreferedCluster: false, }; @@ -161,6 +163,10 @@ export default defineComponent({ this.originalVersion = this.normanCluster?.aksConfig?.kubernetesVersion; } else { this.normanCluster = await store.dispatch('rancher/create', { type: NORMAN.CLUSTER, ...defaultCluster }, { root: true }); + + if (!this.$store.getters['auth/principalId'].includes('local://')) { + this.normanCluster.annotations[CREATOR_PRINCIPAL_ID] = this.$store.getters['auth/principalId']; + } } if (!this.normanCluster.aksConfig) { this.normanCluster['aksConfig'] = { ...defaultAksConfig }; diff --git a/pkg/eks/components/CruEKS.vue b/pkg/eks/components/CruEKS.vue index cf212651d98..e577ceb6ac4 100644 --- a/pkg/eks/components/CruEKS.vue +++ b/pkg/eks/components/CruEKS.vue @@ -27,6 +27,7 @@ import Config from './Config.vue'; import Networking from './Networking.vue'; import AccountAccess from './AccountAccess.vue'; import EKSValidators from '../util/validators'; +import { CREATOR_PRINCIPAL_ID } from '@shell/config/labels-annotations'; const DEFAULT_CLUSTER = { dockerRootDir: '/var/lib/docker', @@ -34,6 +35,7 @@ const DEFAULT_CLUSTER = { enableClusterMonitoring: false, enableNetworkPolicy: false, labels: {}, + annotations: {}, windowsPreferedCluster: false, fleetAgentDeploymentCustomization: {}, clusterAgentDeploymentCustomization: {} @@ -127,6 +129,9 @@ export default defineComponent({ this.originalVersion = this.normanCluster?.eksConfig?.kubernetesVersion || ''; } else { this.normanCluster = await store.dispatch('rancher/create', { type: NORMAN.CLUSTER, ...DEFAULT_CLUSTER }, { root: true }); + if (!this.$store.getters['auth/principalId'].includes('local://')) { + this.normanCluster.annotations[CREATOR_PRINCIPAL_ID] = this.$store.getters['auth/principalId']; + } } if (!this.normanCluster.eksConfig) { diff --git a/pkg/gke/components/CruGKE.vue b/pkg/gke/components/CruGKE.vue index 7e75d8bc072..5735c733a0d 100644 --- a/pkg/gke/components/CruGKE.vue +++ b/pkg/gke/components/CruGKE.vue @@ -37,6 +37,7 @@ import { clusterNameChars, clusterNameStartEnd, requiredInCluster, ipv4WithCidr, ipv4oripv6WithCidr, GKEInitialCount } from '../util/validators'; import { diffUpstreamSpec, syncUpstreamConfig } from '@shell/utils/kontainer'; +import { CREATOR_PRINCIPAL_ID } from '@shell/config/labels-annotations'; const defaultMachineType = 'n1-standard-2'; @@ -124,6 +125,7 @@ const defaultCluster = { enableClusterMonitoring: false, enableNetworkPolicy: false, labels: {}, + annotations: {}, windowsPreferedCluster: false, }; @@ -174,6 +176,9 @@ export default defineComponent({ this.originalVersion = this.normanCluster?.gkeConfig?.kubernetesVersion; } else { this.normanCluster = await store.dispatch('rancher/create', { type: NORMAN.CLUSTER, ...defaultCluster }, { root: true }); + if (!this.$store.getters['auth/principalId'].includes('local://')) { + this.normanCluster.annotations[CREATOR_PRINCIPAL_ID] = this.$store.getters['auth/principalId']; + } } // ensure any fields editable through this UI that have been altered in aws are shown here - see syncUpstreamConfig jsdoc for details if (!this.isNewOrUnprovisioned) { diff --git a/shell/config/labels-annotations.js b/shell/config/labels-annotations.js index e2e0a796ec1..0c353e85bcf 100644 --- a/shell/config/labels-annotations.js +++ b/shell/config/labels-annotations.js @@ -11,6 +11,7 @@ export const CATTLE_PUBLIC_ENDPOINTS = 'field.cattle.io/publicEndpoints'; export const TARGET_WORKLOADS = 'field.cattle.io/targetWorkloadIds'; export const UI_MANAGED = 'management.cattle.io/ui-managed'; export const CREATOR_ID = 'field.cattle.io/creatorId'; +export const CREATOR_PRINCIPAL_ID = 'field.cattle.io/creator-principal-name'; export const RESOURCE_QUOTA = 'field.cattle.io/resourceQuota'; export const AZURE_MIGRATED = 'auth.cattle.io/azuread-endpoint-migrated'; export const WORKSPACE_ANNOTATION = 'objectset.rio.cattle.io/id'; diff --git a/shell/edit/management.cattle.io.project.vue b/shell/edit/management.cattle.io.project.vue index 54ebc1159ea..4a67ab6415f 100644 --- a/shell/edit/management.cattle.io.project.vue +++ b/shell/edit/management.cattle.io.project.vue @@ -14,7 +14,7 @@ import { MANAGEMENT } from '@shell/config/types'; import { NAME } from '@shell/config/product/explorer'; import { PROJECT_ID, _VIEW, _CREATE, _EDIT } from '@shell/config/query-params'; import ProjectMembershipEditor, { canViewProjectMembershipEditor } from '@shell/components/form/Members/ProjectMembershipEditor'; - +import { CREATOR_PRINCIPAL_ID } from '@shell/config/labels-annotations'; import { HARVESTER_NAME as HARVESTER } from '@shell/config/features'; import { Banner } from '@components/Banner'; @@ -103,6 +103,9 @@ export default { this.value.metadata['namespace'] = this.$store.getters['currentCluster'].id; this.value['spec'] = this.value.spec || {}; this.value.spec['containerDefaultResourceLimit'] = this.value.spec.containerDefaultResourceLimit || {}; + if (!this.$store.getters['auth/principalId'].includes('local://')) { + this.value.metadata.annotations[CREATOR_PRINCIPAL_ID] = this.$store.getters['auth/principalId']; + } }, methods: { async save(saveCb) {