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

fix: check authorities in app adapter [LIBS-370] #757

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(adapter): use proper app authority name
  • Loading branch information
KaiVandivier committed Oct 17, 2022
commit b03b8584489b3bbbe887917f63f6fedc9b15d47f
61 changes: 27 additions & 34 deletions adapter/src/components/AuthBoundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { LoadingMask } from './LoadingMask'

// TODO: Remove useVerifyLatestUser
// TODO: add actual appName to config; rename existing to appTitle (& refactor)
// - will also need to change app-runtime and headerbar-ui APIs
// TODO: Remove useVerifyLatestUser.js (and in app wrapper)

const LATEST_USER_KEY = 'dhis2.latestUser'
const IS_PRODUCTION_ENV = process.env.NODE_ENV === 'production'

const APP_MANAGER_AUTHORITY = 'M_dhis-web-maintenance-appmanager'
const REQUIRED_APP_AUTHORITY = process.env.REACT_APP_DHIS2_APP_AUTH_NAME
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in CLI


const USER_QUERY = {
user: {
Expand All @@ -18,21 +22,6 @@ const USER_QUERY = {
},
}

const LATEST_USER_KEY = 'dhis2.latestUser'

const getRequiredAppAuthority = (appName) => {
// TODO: this only works for installed, non-core apps. Need other logic for those (dhis-web-app-name)
// Maybe add this logic to CLI add this to config, instead of needing more env vars here
// Need 'coreApp', 'name', 'title' (rename current appName to appTitle)
return (
'M_' +
appName
.trim()
.replaceAll(/[^a-zA-Z0-9\s]/g, '')
.replaceAll(/\s/g, '_')
)
}

async function clearCachesIfUserChanged({ currentUserId, pwaEnabled }) {
const latestUserId = localStorage.getItem(LATEST_USER_KEY)
if (currentUserId !== latestUserId) {
Expand All @@ -46,6 +35,20 @@ async function clearCachesIfUserChanged({ currentUserId, pwaEnabled }) {
}
}

const isAppAvailable = (authorities) => {
// Skip check on dev
// TODO: should we check on dev environments too?
if (!IS_PRODUCTION_ENV) {
return true
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious what others think -- most app devs probably have admin privileges, but if they don't have app management authority, it's possible that someone might end up working on an a new app which they don't have authorities for.

Someone might be able to bypass their authorities limitations by running an app locally though?...

Currently I guess we don't do any checks like this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(You can make testing easier by commenting out the return true to run it on dev)

Copy link
Contributor

@HendrikThePendric HendrikThePendric Oct 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially wasn't sold on the idea of always returning true in a development environment, but think I'm coming around to it.

Full reload From SW
Production Server renders no-access page App-shell renders no-access view
Development Dev can access app Dev can access app

This is actually very consistent and I like it.

// Check for three possible authorities
return authorities.some((authority) =>
['ALL', APP_MANAGER_AUTHORITY, REQUIRED_APP_AUTHORITY].includes(
authority
)
)
}

/**
* This hook is used to clear sensitive caches if a user other than the one
* that cached that data logs in
Expand All @@ -56,8 +59,6 @@ export function AuthBoundary({ children }) {
const [finished, setFinished] = useState(false)
const { loading, error, data } = useDataQuery(USER_QUERY, {
onComplete: async ({ user }) => {
console.log({ authorities: user.authorities })

await clearCachesIfUserChanged({
currentUserId: user.id,
pwaEnabled,
Expand All @@ -74,21 +75,13 @@ export function AuthBoundary({ children }) {
throw new Error('Failed to fetch user ID: ' + error)
}

if (data) {
const userHasAllAuthority = data.user.authorities.includes('ALL')

const requiredAppAuthority = getRequiredAppAuthority(appName)
console.log({ requiredAppAuthority })

const userHasRequiredAppAuthority =
data.user.authorities.includes(requiredAppAuthority)
if (!userHasAllAuthority && !userHasRequiredAppAuthority) {
// TODO: better UI element than error boundary?
throw new Error('Forbidden: not authorized to view this app')
}
if (isAppAvailable(data.user.authorities)) {
return children
} else {
throw new Error(
`Forbidden: you don't have access to the ${appName} app`
)
}

return children
}
AuthBoundary.propTypes = {
children: PropTypes.node,
Expand Down
30 changes: 30 additions & 0 deletions cli/src/lib/formatAppAuthName.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const APP_AUTH_PREFIX = 'M_'
const DHIS_WEB = 'dhis-web-'

/**
* Returns the string that identifies the 'App view permission'
* required to view the app
*
* Ex: coreApp && name = 'data-visualizer': authName = 'M_dhis-web-data-visualizer'
* Ex: name = 'pwa-example': authName = 'M_pwaexample'
* Ex: name = 'BNA Action Tracker': authName = 'M_BNA_Action_Tracker'
*/
const formatAppAuthName = (config) => {
if (config.coreApp) {
// TODO: Verify this formatting - are there any transformations,
// or do we trust that it's lower-case and hyphenated?
return APP_AUTH_PREFIX + DHIS_WEB + config.name
}
Copy link
Contributor Author

@KaiVandivier KaiVandivier Oct 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about whether this formatting of core app names is correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the user app I am also inferring specific types of authorities based on their naming scheme. This has been in production for about 4 years now, so this suggests to me the naming scheme is consistent enough. You can also review the response of this requests: https://play.dhis2.org/dev/api/40/authorities?fields=id,name&paging=false

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do need to do transformation here, as the "name" in the d2 config is NOT necessarily what's used for the authority (or URL name) - see here. We already replicate this logic in the platform here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see we're doing that below - but we should also do it for core apps.

Copy link
Contributor Author

@KaiVandivier KaiVandivier Oct 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, notice that the formatting is different between those two things you linked, namely around hyphens -- core apps seem to be getting a different treatment? But I didn't find what exactly it is

Copy link
Contributor Author

@KaiVandivier KaiVandivier Oct 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some formatting insurance in this commit


// This formatting is drawn from https://github.com/dhis2/dhis2-core/blob/master/dhis-2/dhis-api/src/main/java/org/hisp/dhis/appmanager/App.java#L494-L499
// (replaceAll is only introduced in Node 15)
return (
APP_AUTH_PREFIX +
config.name
.trim()
.replace(/[^a-zA-Z0-9\s]/g, '')
.replace(/\s/g, '_')
)
}

module.exports = formatAppAuthName
26 changes: 26 additions & 0 deletions cli/src/lib/formatAppAuthName.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const formatAppAuthName = require('./formatAppAuthName.js')

describe('core app handling', () => {
it('should handle core apps', () => {
const config = { coreApp: true, name: 'data-visualizer' }
const formattedAuthName = formatAppAuthName(config)

expect(formattedAuthName).toBe('M_dhis-web-data-visualizer')
})
})

describe('non-core app handling', () => {
it('should handle app names with hyphens', () => {
const config = { name: 'hyphenated-string-example' }
const formattedAuthName = formatAppAuthName(config)

expect(formattedAuthName).toBe('M_hyphenatedstringexample')
})

it('should handle app names with capitals and spaces', () => {
const config = { name: 'Multi Word App Name' }
const formattedAuthName = formatAppAuthName(config)

expect(formattedAuthName).toBe('M_Multi_Word_App_Name')
})
})
2 changes: 2 additions & 0 deletions cli/src/lib/shell/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { exec } = require('@dhis2/cli-helpers-engine')
const formatAppAuthName = require('../formatAppAuthName')
const { getPWAEnvVars } = require('../pwa')
const bootstrap = require('./bootstrap')
const getEnv = require('./env')
Expand All @@ -7,6 +8,7 @@ module.exports = ({ config, paths }) => {
const baseEnvVars = {
name: config.title,
version: config.version,
auth_name: formatAppAuthName(config),
}

return {
Expand Down