|
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 |
|
17 | | -import express from 'express'; |
| 17 | +import express, { Request } from 'express'; |
18 | 18 | import Router from 'express-promise-router'; |
19 | 19 |
|
20 | | -import { HttpAuthService } from '@backstage/backend-plugin-api'; |
| 20 | +import { |
| 21 | + HttpAuthService, |
| 22 | + PermissionsService, |
| 23 | +} from '@backstage/backend-plugin-api'; |
| 24 | +import { |
| 25 | + AuthorizeResult, |
| 26 | + BasicPermission, |
| 27 | + PolicyDecision, |
| 28 | + ResourcePermission, |
| 29 | +} from '@backstage/plugin-permission-common'; |
21 | 30 |
|
22 | 31 | import { |
23 | 32 | decodeGetEntitiesRequest, |
24 | 33 | decodeGetEntityFacetsRequest, |
| 34 | + extensionPluginCreatePermission, |
| 35 | + extensionPluginReadPermission, |
25 | 36 | MarketplaceApi, |
| 37 | + MarketplacePlugin, |
| 38 | + RESOURCE_TYPE_EXTENSION_PLUGIN, |
| 39 | + extensionPermissions, |
26 | 40 | } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; |
| 41 | +import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; |
27 | 42 | import { createSearchParams } from './utils/createSearchParams'; |
28 | 43 | import { removeVerboseSpecContent } from './utils/removeVerboseSpecContent'; |
| 44 | +import { rules as extensionRules } from './permissions/rules'; |
| 45 | +import { matches } from './utils/permissionUtils'; |
29 | 46 |
|
30 | 47 | export async function createRouter({ |
31 | 48 | marketplaceApi, |
| 49 | + httpAuth, |
| 50 | + permissions, |
32 | 51 | }: { |
33 | 52 | httpAuth: HttpAuthService; |
34 | 53 | marketplaceApi: MarketplaceApi; |
| 54 | + permissions: PermissionsService; |
35 | 55 | }): Promise<express.Router> { |
36 | 56 | const router = Router(); |
| 57 | + const permissionsIntegrationRouter = createPermissionIntegrationRouter({ |
| 58 | + resourceType: RESOURCE_TYPE_EXTENSION_PLUGIN, |
| 59 | + permissions: extensionPermissions, |
| 60 | + rules: Object.values(extensionRules), |
| 61 | + }); |
37 | 62 | router.use(express.json()); |
| 63 | + router.use(permissionsIntegrationRouter); |
| 64 | + |
| 65 | + const authorizeConditional = async ( |
| 66 | + request: Request, |
| 67 | + permission: |
| 68 | + | ResourcePermission<'extension-plugin' | 'extension-package'> |
| 69 | + | BasicPermission, |
| 70 | + ) => { |
| 71 | + const credentials = await httpAuth.credentials(request); |
| 72 | + let decision: PolicyDecision; |
| 73 | + if (permission.type === 'resource') { |
| 74 | + decision = ( |
| 75 | + await permissions.authorizeConditional([{ permission }], { |
| 76 | + credentials, |
| 77 | + }) |
| 78 | + )[0]; |
| 79 | + } else { |
| 80 | + decision = ( |
| 81 | + await permissions.authorize([{ permission }], { |
| 82 | + credentials, |
| 83 | + }) |
| 84 | + )[0]; |
| 85 | + } |
| 86 | + |
| 87 | + return decision; |
| 88 | + }; |
38 | 89 |
|
39 | 90 | router.get('/collections', async (req, res) => { |
40 | 91 | const request = decodeGetEntitiesRequest(createSearchParams(req)); |
@@ -108,6 +159,112 @@ export async function createRouter({ |
108 | 159 | res.json(plugin); |
109 | 160 | }); |
110 | 161 |
|
| 162 | + router.get( |
| 163 | + '/plugin/:namespace/:name/configuration/authorize', |
| 164 | + async (req, res) => { |
| 165 | + const [readDecision, installDecision] = await Promise.all([ |
| 166 | + authorizeConditional(req, extensionPluginReadPermission), |
| 167 | + authorizeConditional(req, extensionPluginCreatePermission), |
| 168 | + ]); |
| 169 | + if ( |
| 170 | + readDecision.result === AuthorizeResult.DENY && |
| 171 | + installDecision.result === AuthorizeResult.DENY |
| 172 | + ) { |
| 173 | + res.status(403); |
| 174 | + return; |
| 175 | + } |
| 176 | + |
| 177 | + const authorizedActions: string[] = []; |
| 178 | + let plugin: MarketplacePlugin; |
| 179 | + |
| 180 | + const evaluateConditional = async ( |
| 181 | + decision: PolicyDecision, |
| 182 | + action: string, |
| 183 | + ) => { |
| 184 | + if (decision.result === AuthorizeResult.CONDITIONAL) { |
| 185 | + if (!plugin) { |
| 186 | + plugin = await marketplaceApi.getPluginByName( |
| 187 | + req.params.namespace, |
| 188 | + req.params.name, |
| 189 | + ); |
| 190 | + } |
| 191 | + if (matches(plugin, decision.conditions)) { |
| 192 | + authorizedActions.push(action); |
| 193 | + } |
| 194 | + } else if (decision.result === AuthorizeResult.ALLOW) { |
| 195 | + authorizedActions.push(action); |
| 196 | + } |
| 197 | + }; |
| 198 | + |
| 199 | + await Promise.all([ |
| 200 | + evaluateConditional(readDecision, 'read'), |
| 201 | + evaluateConditional(installDecision, 'create'), |
| 202 | + ]); |
| 203 | + |
| 204 | + if (authorizedActions.length === 0) { |
| 205 | + res.status(403); |
| 206 | + return; |
| 207 | + } |
| 208 | + res.status(200).json({ authorizedActions }); |
| 209 | + }, |
| 210 | + ); |
| 211 | + |
| 212 | + router.get('/plugin/:namespace/:name/configuration', async (req, res) => { |
| 213 | + const readDecision = await authorizeConditional( |
| 214 | + req, |
| 215 | + extensionPluginReadPermission, |
| 216 | + ); |
| 217 | + if (readDecision.result === AuthorizeResult.DENY) { |
| 218 | + res.status(403).json({ error: 'Forbidden' }); |
| 219 | + return; |
| 220 | + } |
| 221 | + |
| 222 | + const plugin = await marketplaceApi.getPluginByName( |
| 223 | + req.params.namespace, |
| 224 | + req.params.name, |
| 225 | + ); |
| 226 | + |
| 227 | + const hasReadAccess = |
| 228 | + readDecision.result === AuthorizeResult.ALLOW || |
| 229 | + (readDecision.result === AuthorizeResult.CONDITIONAL && |
| 230 | + matches(plugin, readDecision.conditions)); |
| 231 | + if (!hasReadAccess) { |
| 232 | + res.status(403).json({ error: 'Forbidden' }); |
| 233 | + return; |
| 234 | + } |
| 235 | + |
| 236 | + res.status(200).json({}); // This should return the configuration in YAML string |
| 237 | + }); |
| 238 | + |
| 239 | + router.post('/plugin/:namespace/:name/configuration', async (req, res) => { |
| 240 | + // installs the plugin |
| 241 | + const installDecision = await authorizeConditional( |
| 242 | + req, |
| 243 | + extensionPluginCreatePermission, |
| 244 | + ); |
| 245 | + if (installDecision.result === AuthorizeResult.DENY) { |
| 246 | + res.status(403).json({ error: 'Forbidden' }); |
| 247 | + return; |
| 248 | + } |
| 249 | + |
| 250 | + const plugin = await marketplaceApi.getPluginByName( |
| 251 | + req.params.namespace, |
| 252 | + req.params.name, |
| 253 | + ); |
| 254 | + |
| 255 | + const hasInstallAccess = |
| 256 | + installDecision.result === AuthorizeResult.ALLOW || |
| 257 | + (installDecision.result === AuthorizeResult.CONDITIONAL && |
| 258 | + matches(plugin, installDecision.conditions)); |
| 259 | + |
| 260 | + if (!hasInstallAccess) { |
| 261 | + res.status(403).json({ error: 'Forbidden' }); |
| 262 | + return; |
| 263 | + } |
| 264 | + |
| 265 | + res.status(200).json({}); |
| 266 | + }); |
| 267 | + |
111 | 268 | router.get('/plugin/:namespace/:name/packages', async (req, res) => { |
112 | 269 | const packages = await marketplaceApi.getPluginPackages( |
113 | 270 | req.params.namespace, |
|
0 commit comments