Skip to content

Commit

Permalink
fga self subject rules review
Browse files Browse the repository at this point in the history
  • Loading branch information
petersutter committed Dec 10, 2024
1 parent 8754a10 commit 2a8af8b
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 23 deletions.
66 changes: 64 additions & 2 deletions backend/lib/openfga/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
// SPDX-License-Identifier: Apache-2.0
//

const _ = require('lodash')

const { extend } = require('@gardener-dashboard/request')

const config = require('../config')
const logger = require('../logger')
const cache = require('../cache')
const getPermissionMappings = require('./permissionMappings')

const {
fgaApiUrl,
fgaStoreId,
fgaApiToken,
fgaAuthorizationModelId,
} = config

const fgaClient = fgaApiUrl && fgaStoreId && fgaApiToken
Expand Down Expand Up @@ -70,7 +74,7 @@ async function listProjects (username, relation = 'viewer') {
type,
},
})
logger.debug('Openfga response objects: %s', objects)
logger.debug('OpenFGA list projects response objects: %s', objects)
const projects = []
for (const object of objects) {
const [prefix, namespace] = object.split(':')
Expand All @@ -79,16 +83,74 @@ async function listProjects (username, relation = 'viewer') {
const project = cache.findProjectByNamespace(namespace)
projects.push(project.metadata.name)
} catch (err) {
logger.debug('Openfga gardener project "%s" not found', namespace)
logger.debug('OpenFGA gardener project "%s" not found', namespace)
}
}
}
return projects
}

function batchCheck (checks) {
const body = {
checks,
}

if (fgaAuthorizationModelId) {
body.authorization_model_id = fgaAuthorizationModelId
}

return fgaClient.request('batch-check', {
method: 'POST',
json: body,
})
}

function getProjectName (namespace) {
try {
return cache.findProjectByNamespace(namespace).metadata.name
} catch (err) {
// ignore error when project is not found
return null
}
}

async function getDerivedResourceRules (username, namespace, accountId) {
const projectName = getProjectName(namespace)
const permissionMappings = getPermissionMappings(accountId, projectName)
const checks = permissionMappings.map(({ relation, object }) => ({
tuple_key: {
user: `user:${username}`,
relation,
object,
},
correlation_id: relation,
}))

let fgaResult
try {
const response = await batchCheck(checks)
fgaResult = response.result
logger.debug('OpenFGA batch check result: %s', JSON.stringify(fgaResult))
} catch (error) {
logger.error('Error performing batch permission checks:', error)
throw new Error('Error performing batch permission checks')
}

const isAllowed = ({ relation: correlationId }) => {
return _.get(fgaResult, [correlationId, 'allowed'], false)
}

return _
.chain(permissionMappings)
.filter(isAllowed)
.map(({ verbs, apiGroups, resources }) => ({ verbs, apiGroups, resources }))
.value()
}

module.exports = {
client: fgaClient,
listProjects,
writeProject,
deleteProject,
getDerivedResourceRules,
}
154 changes: 154 additions & 0 deletions backend/lib/openfga/permissionMappings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0
//

module.exports = function getPermissionMappings (accountId, projectName) {
const accountPermissions = getAccountPermissions(accountId)
const projectPermissions = getProjectPermissions(projectName)

return [...accountPermissions, ...projectPermissions]
}

function getAccountPermissions (accountId) {
if (!accountId) {
return []
}

return [
{
verbs: ['create'],
apiGroups: ['core.gardener.cloud'],
resources: ['projects'],
relation: 'gardener_project_create',
correlationId: 'g_project_create',
object: `account:${accountId}`,
},
]
}

function getProjectPermissions (projectName) {
if (!projectName) {
return []
}

return [
// Shoots
{
verbs: ['create'],
apiGroups: ['core.gardener.cloud'],
resources: ['shoots'],
relation: 'gardener_shoot_create',
object: `gardener_project:${projectName}`,
},
{
verbs: ['patch'],
apiGroups: ['core.gardener.cloud'],
resources: ['shoots'],
relation: 'gardener_shoot_patch',
object: `gardener_project:${projectName}`,
},
{
verbs: ['delete'],
apiGroups: ['core.gardener.cloud'],
resources: ['shoots'],
relation: 'gardener_shoot_delete',
object: `gardener_project:${projectName}`,
},
{
verbs: ['patch'],
apiGroups: ['core.gardener.cloud'],
resources: ['shoots/binding'],
relation: 'gardener_shoot_binding_patch',
object: `gardener_project:${projectName}`,
},
// Terminals
{
verbs: ['create'],
apiGroups: ['dashboard.gardener.cloud'],
resources: ['terminals'],
relation: 'gardener_terminal_create',
object: `gardener_project:${projectName}`,
},
// Secrets
{
verbs: ['list'],
apiGroups: [''],
resources: ['secrets'],
relation: 'gardener_secrets_get',
object: `gardener_project:${projectName}`,
},
{
verbs: ['create'],
apiGroups: [''],
resources: ['secrets'],
relation: 'gardener_secrets_create',
object: `gardener_project:${projectName}`,
},
{
verbs: ['patch'],
apiGroups: [''],
resources: ['secrets'],
relation: 'gardener_secrets_patch',
object: `gardener_project:${projectName}`,
},
{
verbs: ['delete'],
apiGroups: [''],
resources: ['secrets'],
relation: 'gardener_secrets_delete',
object: `gardener_project:${projectName}`,
},
// Service Accounts
{
verbs: ['create'],
apiGroups: [''],
resources: ['serviceaccounts/token'],
relation: 'gardener_token_request_create',
object: `gardener_project:${projectName}`,
},
{
verbs: ['create'],
apiGroups: [''],
resources: ['serviceaccounts'],
relation: 'gardener_service_account_create',
object: `gardener_project:${projectName}`,
},
{
verbs: ['patch'],
apiGroups: [''],
resources: ['serviceaccounts'],
relation: 'gardener_service_account_patch',
object: `gardener_project:${projectName}`,
},
{
verbs: ['delete'],
apiGroups: [''],
resources: ['serviceaccounts'],
relation: 'gardener_service_account_delete',
object: `gardener_project:${projectName}`,
},
// Projects
{
verbs: ['patch'],
apiGroups: ['core.gardener.cloud'],
resources: ['projects'],
relation: 'gardener_project_patch',
object: `gardener_project:${projectName}`,
},
{
verbs: ['delete'],
apiGroups: ['core.gardener.cloud'],
resources: ['projects'],
relation: 'gardener_project_delete',
object: `gardener_project:${projectName}`,
},
{
verbs: ['manage-members'],
apiGroups: ['core.gardener.cloud'],
resources: ['projects'],
relation: 'gardener_project_members_manage',
object: `gardener_project:${projectName}`,
},
]
}
7 changes: 5 additions & 2 deletions backend/lib/routes/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ router.route('/subjectrules')
.post(async (req, res, next) => {
try {
const user = req.user || {}
const { namespace } = req.body
const result = await authorization.selfSubjectRulesReview(user, namespace)
const {
namespace,
accountId,
} = req.body
const result = await authorization.selfSubjectRulesReview(user, namespace, accountId)
res.send(result)
} catch (err) {
next(err)
Expand Down
56 changes: 48 additions & 8 deletions backend/lib/services/authorization.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
'use strict'

const { Resources, createClient } = require('@gardener-dashboard/kube-client')
const openfga = require('../openfga')
const cache = require('../cache')
const logger = require('../logger')

async function hasAuthorization (user, { resourceAttributes, nonResourceAttributes }) {
if (!user) {
Expand Down Expand Up @@ -150,7 +153,7 @@ exports.canGetSecret = function (user, namespace, name) {
/*
SelfSubjectRulesReview should only be used to hide/show actions or views on the UI and not for authorization checks.
*/
exports.selfSubjectRulesReview = async function (user, namespace) {
exports.selfSubjectRulesReview = async function (user, namespace, accountId) {
if (!user) {
return false
}
Expand All @@ -163,13 +166,50 @@ exports.selfSubjectRulesReview = async function (user, namespace) {
namespace,
},
}
const {
status: {
resourceRules,
nonResourceRules,
incomplete,
evaluationError,

const [
{
status: {
resourceRules: k8sResourceRules = [],
nonResourceRules: k8sNonResourceRules = [],
incomplete: k8sIncomplete = false,
evaluationError: k8sEvaluationError = null,
} = {},
},
{
resourceRules: fgaResourceRules = [],
nonResourceRules: fgaNonResourceRules = [],
incomplete: fgaIncomplete = false,
evaluationError: fgaEvaluationError = null,
} = {},
} = await client['authorization.k8s.io'].selfsubjectrulesreviews.create(body)
] = await Promise.all([
client['authorization.k8s.io'].selfsubjectrulesreviews.create(body),
fgaSelfSubjectRulesReview(user, namespace, accountId),
])

const resourceRules = [...k8sResourceRules, ...fgaResourceRules]
const nonResourceRules = [...k8sNonResourceRules, ...fgaNonResourceRules]
const incomplete = k8sIncomplete || fgaIncomplete
const evaluationError = [k8sEvaluationError, fgaEvaluationError].filter(Boolean).join(' | ') || null

return { resourceRules, nonResourceRules, incomplete, evaluationError }
}

async function fgaSelfSubjectRulesReview (user, namespace, accountId) {
if (!openfga.client || !accountId) {
return
}
const username = user.id
try {
const resourceRules = await openfga.getDerivedResourceRules(username, namespace, accountId)
return {
resourceRules,
}
} catch (error) {
logger.debug('Error while fetching FGA derived resource rules: %s', error.message)
return {
incomplete: true,
evaluationError: error.message,
}
}
}
8 changes: 6 additions & 2 deletions frontend/src/composables/useApi/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,14 @@ export function createTokenReview (data) {
return createResource('/auth', data)
}

export function getSubjectRules (options) {
const namespace = options?.namespace ?? 'default'
export function getSubjectRules (options = {}) {
const {
namespace = 'default',
accountId,
} = options
return callResourceMethod('/api/user/subjectrules', {
namespace,
accountId,
})
}

Expand Down
5 changes: 4 additions & 1 deletion frontend/src/router/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,11 @@ export function createGlobalResolveGuards () {
}

try {
const openMFP = useOpenMFP()
const accountId = openMFP.accountId.value ?? to.query.accountId

const namespace = to.params.namespace ?? to.query.namespace
await refreshRules(authzStore, namespace)
await refreshRules(authzStore, namespace, accountId)

if (namespace && namespace !== '_all' && !projectStore.namespaces.includes(namespace)) {
authzStore.$reset()
Expand Down
Loading

0 comments on commit 2a8af8b

Please sign in to comment.