Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Member roles #642

Merged
merged 14 commits into from
Mar 30, 2020
11 changes: 11 additions & 0 deletions backend/lib/routes/members.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ router.route('/:name')
next(err)
}
})
.put(async (req, res, next) => {
try {
const user = req.user
const namespace = req.params.namespace
const name = req.params.name
const body = req.body
res.send(await members.update({ user, namespace, name, body }))
} catch (err) {
next(err)
}
})
.delete(async (req, res, next) => {
try {
const user = req.user
Expand Down
60 changes: 49 additions & 11 deletions backend/lib/services/members.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

const _ = require('lodash')
const config = require('../config')
const { decodeBase64 } = require('../utils')
const { decodeBase64, toOneMemberRoleArray, toMemberRoleRolesArrays } = require('../utils')
const { isHttpError } = require('../kubernetes-client')
const { dumpKubeconfig } = require('../kubernetes-config')
const { Conflict, NotFound } = require('../errors.js')
Expand All @@ -44,9 +44,9 @@ function fromResource (project = {}, serviceAccounts = []) {
.chain(project)
.get('spec.members')
.filter(['kind', 'User'])
.map('name')
.map(username => ({
.map(({ name: username, role, roles }) => ({
grolu marked this conversation as resolved.
Show resolved Hide resolved
username,
roles: toOneMemberRoleArray(role, roles),
...serviceAccountsMetadata[username]
}))
.value()
Expand Down Expand Up @@ -76,19 +76,21 @@ async function deleteServiceaccount (client, { namespace, name }) {
}
}

async function setProjectMember (client, { namespace, name }) {
async function setProjectMember (client, { namespace, name, roles: memberRoles }) {
// get project
const project = await client.getProjectByNamespace(namespace)
// get project members from project
const members = _.slice(project.spec.members, 0)
const members = [...project.spec.members]
if (_.find(members, ['name', name])) {
throw new Conflict(`User '${name}' is already member of this project`)
}
const { role, roles } = toMemberRoleRolesArrays(memberRoles)
members.push({
kind: 'User',
name,
apiGroup: 'rbac.authorization.k8s.io',
role: 'admin'
role,
roles
})
const body = {
spec: {
Expand All @@ -98,11 +100,31 @@ async function setProjectMember (client, { namespace, name }) {
return client['core.gardener.cloud'].projects.mergePatch(project.metadata.name, body)
}

async function updateProjectMemberRoles (client, { namespace, name, roles: memberRoles }) {
// get project
const project = await client.getProjectByNamespace(namespace)
// get project members from project
const members = [...project.spec.members]
const member = _.find(members, ['name', name])
if (!member) {
throw new NotFound(`User '${name}' is not a member of this project`)
}
const { role, roles } = toMemberRoleRolesArrays(memberRoles)
_.assign(member, { role, roles })

const body = {
spec: {
members
}
}
return client['core.gardener.cloud'].projects.mergePatch(project.metadata.name, body)
}

async function unsetProjectMember (client, { namespace, name }) {
// get project
const project = await client.getProjectByNamespace(namespace)
// get project members from project
const members = _.slice(project.spec.members, 0)
const members = [...project.spec.members]
if (!_.find(members, ['name', name])) {
return project
}
Expand Down Expand Up @@ -151,8 +173,9 @@ exports.get = async function ({ user, namespace, name }) {
const caData = secret.data['ca.crt']
const clusterName = 'garden'
const contextName = `${clusterName}-${projectName}-${name}`
member.kind = 'ServiceAccount'
member.kubeconfig = dumpKubeconfig({

const kind = 'ServiceAccount'
const kubeconfig = dumpKubeconfig({
user: serviceAccountName,
context: contextName,
cluster: clusterName,
Expand All @@ -161,11 +184,17 @@ exports.get = async function ({ user, namespace, name }) {
server,
caData
})

return {
...member,
kind,
kubeconfig
}
}
return member
}

exports.create = async function ({ user, namespace, body: { name } }) {
exports.create = async function ({ user, namespace, body: { name, roles } }) {
grolu marked this conversation as resolved.
Show resolved Hide resolved
const client = user.client

const [, serviceaccountNamespace, serviceaccountName] = /^system:serviceaccount:([^:]+):([^:]+)$/.exec(name) || []
Expand All @@ -178,7 +207,16 @@ exports.create = async function ({ user, namespace, body: { name } }) {
}

// assign user to project
const project = await setProjectMember(client, { namespace, name })
const project = await setProjectMember(client, { namespace, name, roles })
const { items: serviceAccounts } = await client.core.serviceaccounts.list(namespace)
return fromResource(project, serviceAccounts)
}

exports.update = async function ({ user, namespace, name, body: { roles } }) {
const client = user.client

// update user in project
const project = await updateProjectMemberRoles(client, { namespace, name, roles })
const { items: serviceAccounts } = await client.core.serviceaccounts.list(namespace)
return fromResource(project, serviceAccounts)
}
Expand Down
17 changes: 16 additions & 1 deletion backend/lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,25 @@ function shootHasIssue (shoot) {
return _.get(shoot, ['metadata', 'labels', 'shoot.garden.sapcloud.io/status'], 'healthy') !== 'healthy'
}

function toOneMemberRoleArray(role, roles) {
if (roles) {
// uniq to also support test scenarios, gardener discards duplicate roles
return _.uniq([role, ...roles])
}
return [role]
}

function toMemberRoleRolesArrays(roles) {
const role = _.head(roles) // do not shift role, gardener ignores duplicate role in roles array and will remove role field in future API version
return { role, roles }
}

module.exports = {
decodeBase64,
encodeBase64,
getConfigValue,
getSeedNameFromShoot,
shootHasIssue
shootHasIssue,
toOneMemberRoleArray,
toMemberRoleRolesArrays
}
26 changes: 22 additions & 4 deletions backend/test/acceptance/api.members.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,28 +75,46 @@ module.exports = function ({ agent, k8s, auth }) {
it('should add a project member', async function () {
const bearer = await user.bearer
const name = 'baz@example.org'
k8s.stub.addMember({ bearer, namespace, name })
const roles = ['admin', 'owner']
grolu marked this conversation as resolved.
Show resolved Hide resolved
k8s.stub.addMember({ bearer, namespace, name, roles })
const res = await agent
.post(`/api/namespaces/${namespace}/members`)
.set('cookie', await user.cookie)
.send({ metadata, name })
.send({ metadata, name, roles })

expect(res).to.have.status(200)
expect(res).to.be.json
expect(res.body).to.eql(_.concat(members, { username: 'baz@example.org' }))
expect(res.body).to.eql(_.concat(members, { username: name, roles }))
})

it('should not add member that is already a project member', async function () {
const bearer = await user.bearer
const name = 'foo@example.org'
k8s.stub.addMember({ bearer, namespace, name })
const roles = ['admin']
k8s.stub.addMember({ bearer, namespace, name, roles })
const res = await agent
.post(`/api/namespaces/${namespace}/members`)
.set('cookie', await user.cookie)
.send({ metadata, name })
expect(res).to.have.status(409)
})

it('should update roles of a project member', async function () {
const bearer = await user.bearer
const name = 'bar@example.org'
const roles = ['newRole']
k8s.stub.updateMember({ bearer, namespace, name, roles })
const res = await agent
.put(`/api/namespaces/${namespace}/members/${name}`)
.set('cookie', await user.cookie)
.send({ metadata, roles })

expect(res).to.have.status(200)
expect(res).to.be.json
const member = _.find(res.body, { username: name })
expect(member).to.eql({ username: name, roles })
})

it('should delete a project member', async function () {
const bearer = await user.bearer
const name = 'bar@example.org'
Expand Down
97 changes: 71 additions & 26 deletions backend/test/support/nocks/k8s.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
const _ = require('lodash')
const nock = require('nock')
const yaml = require('js-yaml')
const { encodeBase64, getSeedNameFromShoot } = require('../../../lib/utils')
const { encodeBase64, getSeedNameFromShoot, toOneMemberRoleArray, toMemberRoleRolesArrays } = require('../../../lib/utils')
const hash = require('object-hash')
const jwt = require('jsonwebtoken')
const { url, auth } = require('../../../lib/kubernetes-config').load()
Expand Down Expand Up @@ -47,8 +47,18 @@ const projectList = [
createdBy: 'bar@example.org',
owner: 'foo@example.org',
members: [
'bar@example.org',
'system:serviceaccount:garden-foo:robot'
{
name: 'foo@example.org',
roles: ['admin', 'owner']
},
{
name: 'bar@example.org',
roles: ['admin']
},
{
name: 'system:serviceaccount:garden-foo:robot',
roles: ['viewer']
}
],
description: 'foo-description',
purpose: 'foo-purpose',
Expand All @@ -59,8 +69,14 @@ const projectList = [
createdBy: 'foo@example.org',
owner: 'bar@example.org',
members: [
'foo@example.org',
'system:serviceaccount:garden-bar:robot'
{
name: 'foo@example.org',
roles: ['admin', 'owner']
},
{
name: 'system:serviceaccount:garden-foo:robot',
roles: ['viewer', 'admin']
}
],
description: 'bar-description',
purpose: 'bar-purpose'
Expand Down Expand Up @@ -309,7 +325,7 @@ function getProjectMembers (project) {
return _
.chain(project)
.get('spec.members')
.map(({ name: username }) => ({ username }))
.map(({ name: username, role, roles }) => ({ username, roles: toOneMemberRoleArray(role, roles) }))
.value()
}

Expand All @@ -326,21 +342,25 @@ function getInfrastructureSecret (namespace, name, profileName, data = {}) {
}
}

function getUser (name) {
return {
function getUser (member) {
const name = member.name || member
const user = {
apiGroup: 'rbac.authorization.k8s.io',
kind: 'User',
name
}
if (member.roles) {
const { role, roles } = toMemberRoleRolesArrays(member.roles)
_.assign(user, { role, roles })
}
return user
}

function getProject ({ name, namespace, createdBy, owner, members = [], description, purpose, phase = 'Ready', costObject = '' }) {
owner = owner || createdBy
namespace = namespace || `garden-${name}`
members = _
.chain(members)
.concat(owner)
.uniq()
.map(getUser)
.value()
owner = getUser(owner)
Expand Down Expand Up @@ -1283,7 +1303,7 @@ const stub = {
}
})
},
addMember ({ bearer, namespace, name: username }) {
addMember ({ bearer, namespace, name: username, roles }) {
const project = readProject(namespace)
const newProject = _.cloneDeep(project)
const name = project.metadata.name
Expand All @@ -1310,6 +1330,31 @@ const stub = {
scope
]
},
updateMember ({ bearer, namespace, name: username, roles }) {
const project = readProject(namespace)
const newProject = _.cloneDeep(project)
const name = project.metadata.name

const scope = nockWithAuthorization(bearer)
.get(`/apis/core.gardener.cloud/v1beta1/projects/${name}`)
.reply(200, () => project)
const existingMember = _.find(project.spec.members, ['name', username])
if (existingMember) {
scope
.patch(`/apis/core.gardener.cloud/v1beta1/projects/${name}`, body => {
_.assign(newProject.spec.members, body.spec.members)
return true
})
.reply(200, () => newProject)
}
getServiceAccountsForNamespace(scope, namespace)
return [
nockWithAuthorization(bearer)
.get(`/api/v1/namespaces/${namespace}`)
.reply(200, () => getProjectNamespace(namespace)),
scope
]
},
removeMember ({ bearer, namespace, name: username }) {
const project = readProject(namespace)
const newProject = _.cloneDeep(project)
Expand Down Expand Up @@ -1405,24 +1450,24 @@ const stub = {
const incomplete = false
if (_.endsWith(payload.id, 'example.org')) {
resourceRules = resourceRules.concat([{
verbs: ['get'],
apiGroups: ['core.gardener.cloud'],
resources: ['projects'],
resourceName: ['foo']
},
{
verbs: ['create'],
apiGroups: ['core.gardener.cloud'],
resources: ['projects']
}
verbs: ['get'],
apiGroups: ['core.gardener.cloud'],
resources: ['projects'],
resourceName: ['foo']
},
{
verbs: ['create'],
apiGroups: ['core.gardener.cloud'],
resources: ['projects']
}
])
} else {
resourceRules = resourceRules.concat([{
verbs: ['get'],
apiGroups: ['core.gardener.cloud'],
resources: ['projects'],
resourceName: ['foo']
}
verbs: ['get'],
apiGroups: ['core.gardener.cloud'],
resources: ['projects'],
resourceName: ['foo']
}
])
grolu marked this conversation as resolved.
Show resolved Hide resolved
}
return {
Expand Down