Skip to content

Commit

Permalink
Merge pull request #4846 from FlowFuse/prevent-viewer-role-users-from…
Browse files Browse the repository at this point in the history
…-getting-a-404-when-accessing-applications

Prevent viewer role users from getting 404 when accesing applications
  • Loading branch information
knolleary authored Dec 2, 2024
2 parents fa4f8c4 + 66ee868 commit 499b7fd
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 115 deletions.
17 changes: 0 additions & 17 deletions frontend/src/mixins/Application.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ export default {
data () {
return {
application: {},
applicationDevices: [],
deviceGroups: [],
applicationInstances: new Map(),
loading: {
deleting: false,
Expand All @@ -23,12 +21,6 @@ export default {
return []
}
return Array.from(this.applicationInstances.values()).filter(el => el)
},
devicesArray () {
return this.applicationDevices
},
deviceGroupsArray () {
return this.deviceGroups || []
}
},
watch: {
Expand All @@ -51,16 +43,7 @@ export default {
await this.$store.dispatch('account/setTeam', this.application.team.slug)
}
const instancesPromise = ApplicationApi.getApplicationInstances(applicationId) // To-do needs to be enriched with instance state
const devicesPromise = ApplicationApi.getApplicationDevices(applicationId)
const deviceData = await devicesPromise
this.applicationDevices = deviceData?.devices
const applicationInstances = await instancesPromise
if (this.features?.deviceGroups && this.team.type.properties.features?.deviceGroups) {
const deviceGroupsData = await ApplicationApi.getDeviceGroups(applicationId)
this.deviceGroups = deviceGroupsData?.groups || []
} else {
this.deviceGroups = []
}

this.applicationInstances = new Map()
applicationInstances.forEach(instance => {
Expand Down
56 changes: 35 additions & 21 deletions frontend/src/pages/application/Activity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import TeamAPI from '../../api/team.js'
import FormHeading from '../../components/FormHeading.vue'
import SectionTopMenu from '../../components/SectionTopMenu.vue'
import AuditLogBrowser from '../../components/audit-log/AuditLogBrowser.vue'
import usePermissions from '../../composables/Permissions.js'
import FfListbox from '../../ui-components/components/form/ListBox.vue'
export default {
Expand All @@ -53,6 +54,10 @@ export default {
required: true
}
},
setup () {
const { hasPermission } = usePermissions()
return { hasPermission }
},
data () {
return {
logEntries: [],
Expand All @@ -65,7 +70,7 @@ export default {
}
},
computed: {
...mapState('account', ['team']),
...mapState('account', ['team', 'teamMembership']),
instanceList () {
return [
{ name: 'This Application', id: '' },
Expand All @@ -87,6 +92,11 @@ export default {
'auditFilters.includeChildren': 'triggerLoad',
team: function () {
this.triggerLoad({ users: true, events: true })
},
teamMembership () {
if (!this.hasPermission('application:audit-log')) {
return this.$router.push({ name: 'Application', params: this.$route.params })
}
}
},
created () {
Expand All @@ -104,30 +114,34 @@ export default {
* @param cursor - cursor to use for pagination
*/
async loadEntries (params = new URLSearchParams(), cursor = undefined) {
const paramScope = (params.has('scope') ? params.get('scope') : this.auditFilters.selectedEventScope) || 'application'
let includeChildren = this.auditFilters.includeChildren
if (params.has('includeChildren')) {
includeChildren = params.get('includeChildren') === 'true'
}
params.set('includeChildren', includeChildren)
params.set('scope', paramScope)
if (this.applicationId) {
let log
if (paramScope === 'application') {
log = (await ApplicationApi.getApplicationAuditLog(this.applicationId, params, cursor, 200))
} else {
const instanceId = this.auditFilters.selectedEventScope
log = (await InstanceApi.getInstanceAuditLog(instanceId, params, cursor, 200))
if (this.hasPermission('application:audit-log')) {
const paramScope = (params.has('scope') ? params.get('scope') : this.auditFilters.selectedEventScope) || 'application'
let includeChildren = this.auditFilters.includeChildren
if (params.has('includeChildren')) {
includeChildren = params.get('includeChildren') === 'true'
}
params.set('includeChildren', includeChildren)
params.set('scope', paramScope)
if (this.applicationId) {
let log
if (paramScope === 'application') {
log = (await ApplicationApi.getApplicationAuditLog(this.applicationId, params, cursor, 200))
} else {
const instanceId = this.auditFilters.selectedEventScope
log = (await InstanceApi.getInstanceAuditLog(instanceId, params, cursor, 200))
}
this.logEntries = log.log
this.associations = includeChildren ? log.associations : null
}
this.logEntries = log.log
this.associations = includeChildren ? log.associations : null
}
},
triggerLoad ({ users = false, events = true } = {}) {
// if `events` is true, call AuditLogBrowser.loadEntries - this will emit 'load-entries' event which calls this.loadEntries with appropriate params
const scope = !this.auditFilters.selectedEventScope ? 'application' : 'project'
events && this.$refs.AuditLog?.loadEntries(scope, this.auditFilters.includeChildren, scope)
users && this.loadUsers()
if (this.hasPermission('application:audit-log')) {
// if `events` is true, call AuditLogBrowser.loadEntries - this will emit 'load-entries' event which calls this.loadEntries with appropriate params
const scope = !this.auditFilters.selectedEventScope ? 'application' : 'project'
events && this.$refs.AuditLog?.loadEntries(scope, this.auditFilters.includeChildren, scope)
users && this.loadUsers()
}
}
}
}
Expand Down
44 changes: 28 additions & 16 deletions frontend/src/pages/application/DeviceGroups.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import ApplicationAPI from '../../api/application.js'
import EmptyState from '../../components/EmptyState.vue'
import FormRow from '../../components/FormRow.vue'
import SectionTopMenu from '../../components/SectionTopMenu.vue'
import usePermissions from '../../composables/Permissions.js'
import Alerts from '../../services/alerts.js'
Expand Down Expand Up @@ -99,6 +100,10 @@ export default {
required: true
}
},
setup () {
const { hasPermission } = usePermissions()
return { hasPermission }
},
data () {
return {
loading: false,
Expand Down Expand Up @@ -138,7 +143,7 @@ export default {
}
},
computed: {
...mapState('account', ['features', 'team']),
...mapState('account', ['features', 'team', 'teamMembership']),
featureEnabledForTeam () {
return !!this.team?.type?.properties?.features?.deviceGroups
},
Expand All @@ -152,6 +157,11 @@ export default {
watch: {
featureEnabled: function (v) {
this.loadDeviceGroups()
},
teamMembership () {
if (!this.hasPermission('application:device-group:list')) {
return this.$router.push({ name: 'Application', params: this.$route.params })
}
}
},
mounted () {
Expand Down Expand Up @@ -191,22 +201,24 @@ export default {
this.$router.push(route)
},
async loadDeviceGroups () {
this.loading = true
ApplicationAPI.getDeviceGroups(this.application.id)
.then((groups) => {
this.deviceGroups = groups.groups
if (this.deviceGroups?.length > 0) {
if (this.hasPermission('application:device-group:list')) {
this.loading = true
ApplicationAPI.getDeviceGroups(this.application.id)
.then((groups) => {
this.deviceGroups = groups.groups
if (this.deviceGroups?.length > 0) {
// if there is no target snapshot set, set it to an empty object so that the `markRaw` function renders _something_ in the table cell
this.deviceGroups.forEach((group) => {
group.targetSnapshot = group.targetSnapshot || {}
})
}
})
.catch((err) => {
console.error(err)
}).finally(() => {
this.loading = false
})
this.deviceGroups.forEach((group) => {
group.targetSnapshot = group.targetSnapshot || {}
})
}
})
.catch((err) => {
console.error(err)
}).finally(() => {
this.loading = false
})
}
}
}
}
Expand Down
65 changes: 31 additions & 34 deletions frontend/src/pages/application/Pipeline/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,55 +27,52 @@ export default {
instances: {
type: Object,
required: true
},
devices: {
type: Array,
required: true
},
deviceGroups: {
type: Array,
required: true
}
},
data: function () {
return {
pipeline: null
pipeline: null,
devices: [],
deviceGroups: []
}
},
watch: {
'application.id': 'loadPipeline'
'application.id': 'fetchData',
'$route.params.pipelineId': 'fetchData'
},
created () {
this.loadPipeline()
this.$watch(
() => this.$route.params.pipelineId,
() => {
if (!this.$route.params.pipelineId) {
return
}
this.loadPipeline()
}
)
async created () {
await this.fetchData()
},
methods: {
async loadPipeline () {
if (!this.application.id) {
return
return Promise.resolve()
}
try {
this.pipeline = await ApplicationApi.getPipeline(this.application.id, this.$route.params.pipelineId)
} catch (err) {
this.$router.push({
name: 'page-not-found',
params: { pathMatch: this.$router.currentRoute.value.path.substring(1).split('/') },
// preserve existing query and hash if any
query: this.$router.currentRoute.value.query,
hash: this.$router.currentRoute.value.hash
return ApplicationApi.getPipeline(this.application.id, this.$route.params.pipelineId)
.then(res => {
this.pipeline = res
})
},
async fetchData () {
return this.loadPipeline()
.then(() => ApplicationApi.getApplicationDevices(this.application.id))
.then(res => {
this.devices = res.devices
})
.then(() => ApplicationApi.getDeviceGroups(this.application.id))
.then((res) => {
this.deviceGroups = res.groups
})
.catch(() => {
this.$router.push({
name: 'page-not-found',
params: { pathMatch: this.$router.currentRoute.value.path.substring(1).split('/') },
// preserve existing query and hash if any
query: this.$router.currentRoute.value.query,
hash: this.$router.currentRoute.value.hash
})
})
}
}
}
}
Expand Down
50 changes: 35 additions & 15 deletions frontend/src/pages/application/Pipelines.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ import PipelineAPI from '../../api/pipeline.js'
import EmptyState from '../../components/EmptyState.vue'
import SectionTopMenu from '../../components/SectionTopMenu.vue'
import PipelineRow from '../../components/pipelines/PipelineRow.vue'
import usePermissions from '../../composables/Permissions.js'
import Alerts from '../../services/alerts.js'
Expand All @@ -127,6 +128,10 @@ export default {
required: true
}
},
setup () {
const { hasPermission } = usePermissions()
return { hasPermission }
},
data () {
return {
loading: false,
Expand All @@ -142,11 +147,24 @@ export default {
}
},
computed: {
...mapState('account', ['features']),
...mapState('account', ['features', 'teamMembership']),
featureEnabled () {
return this.features['devops-pipelines']
}
},
watch: {
teamMembership () {
if (!this.hasPermission('application:pipeline:list')) {
return this.$router.push({ name: 'Application', params: this.$route.params })
}
// Forces to load pipelines when teamMembership changes. When loading the page via url, teamMembership might not be
// loaded by the time the mounted loadPipelines is called and the hasPermission method will return false.
// todo This should be addressed by implementing an application service that bootstrap's the
// app and hydrates vuex stores before attempting to render any data
this.loadPipelines()
}
},
mounted () {
if (this.featureEnabled) {
this.loadPipelines()
Expand Down Expand Up @@ -245,22 +263,24 @@ export default {
}
},
async loadPipelines () {
this.loading = true
if (this.hasPermission('application:pipeline:list')) {
this.loading = true
// getPipelines doesn't include full instance status information, kick this off async
// Not needed for devices as device status is returned as part of pipelines API
this.loadInstanceStatus()
// getPipelines doesn't include full instance status information, kick this off async
// Not needed for devices as device status is returned as part of pipelines API
this.loadInstanceStatus()
ApplicationAPI.getPipelines(this.application.id)
.then((pipelines) => {
this.pipelines = pipelines
this.loadDeviceGroupStatus(this.pipelines)
this.loading = false
})
.catch((err) => {
console.error(err)
this.loading = false
})
ApplicationAPI.getPipelines(this.application.id)
.then((pipelines) => {
this.pipelines = pipelines
this.loadDeviceGroupStatus(this.pipelines)
this.loading = false
})
.catch((err) => {
console.error(err)
this.loading = false
})
}
},
async loadInstanceStatus () {
ApplicationAPI.getApplicationInstancesStatuses(this.application.id)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/application/Snapshots.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export default {
EmptyState
},
mixins: [permissionsMixin],
inheritAttrs: false,
props: {
application: {
type: Object,
Expand Down
Loading

0 comments on commit 499b7fd

Please sign in to comment.