Skip to content

Commit

Permalink
Merge branch 'main' into 3973-create-instances-with-predefined-bluepr…
Browse files Browse the repository at this point in the history
…ints
  • Loading branch information
Steve-Mcl committed Jun 25, 2024
2 parents 7e32f6c + a0ea7f0 commit ff17652
Show file tree
Hide file tree
Showing 13 changed files with 112 additions and 139 deletions.
68 changes: 0 additions & 68 deletions .github/scripts/backport.sh

This file was deleted.

22 changes: 0 additions & 22 deletions .github/workflows/backport.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/branch-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:

- name: Build container image
id: build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: "./ci/Dockerfile"
Expand Down
7 changes: 6 additions & 1 deletion docs/admin/sso/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ as Google Workspace, or using LDAP against a directory service provider.
The platform can be configured with multiple SSO configurations and uses the
user's email domain to identify which provider should be used.

The user must already exist on the FlowFuse platform before they can sign in via SSO.
**The user must already exist on the FlowFuse platform before they can sign in via SSO.**

A user will have to register on the platform, providing a temporary password in order to create an account.
They will then be able to login via their SSO provider. Support for auto-provisioning accounts is planned
for the future - see [this issue for details](https://github.com/FlowFuse/flowfuse/issues/4051).


Admin users will still be able to log in with their original FlowFuse username/password - this ensures
they don't get locked out of the platform if there is a problem with the SSO configuration.
Expand Down
23 changes: 19 additions & 4 deletions docs/admin/sso/saml.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,18 +128,33 @@ with FlowFuse SAML SSO.

Microsoft provide a guide for creating a custom SAML Application [here](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal).

The following table maps the Entra terminology to the FlowFuse settings.
The following tables map the Entra terminology to the FlowFuse settings.

FlowFuse Setting | Entra Setting
----|----
`ACS URL` | `Reply URL (Assertion Consumer Service URL)`
`Identity Provider Single Sign-On URL` | `Login URL`
`Identity Provider Issuer ID / URL` | `Microsoft Entra Identifier`
`Identity Provider Single Sign-On URL` | `Login URL`
`X.509 Certificate Public Key` | `Certificate (Base64)`

Within the `SAML Signing Certificate` configuration, the `Signing Option` must be set to `Sign SAML response and assertion`.
Follow these steps to properly configure SAML SSO for Microsoft Entra:

1. In FlowFuse:
1. Create a draft SSO configuration in FlowFuse with the appropriate email domain
2. In Entra:
1. Create a SAML application - use the guide linked above for more information
2. Copy the following values from the FlowFuse SSO configuration into the corresponding Entra configuration:
1. Set `Reply URL` to the value of `ACS URL`
2. Set `Identifier (Entity ID)` to the value of `Entity ID/Issuer`
3. Within the `SAML Signing Certificate` configuration, the `Signing Option` must be set to `Sign SAML response and assertion`.
4. The `Unique User Identifier (Name ID)` claim must be configured to return the value of the `user.mail` source attribute.
5. Download the `Federation Metadata XML` file from the `SAML Certificates` section of the Entra application.
3. In FlowFuse:
1. From the metadata XML file, copy the follow properties into the FlowFuse SSO configuration:
1. Set `Identity Provider Single Sign-On URL` to the value of the `Location` attribute of the `<SingleSignOnService>` tag. This should look like `https://login.microsoftonline.com/<app-id>/saml2`.
2. Set `Identity Provider Issuer ID / URL` to the value of the `entityID` attribute of the `<EntityDescriptor>` tag. This should look like `https://sts.windows.net/<app-id>/`
3. Set `X.509 Certificate Public Key` to the value of the `<ds:X509Certificate>` tag. This does *not* need to have the `-----BEGIN CERTIFICATE-----/-----END CERTIFICATE-----` wrapper.

The `Unique User Identifier (Name ID)` claim must be configured to return the value of the `user.mail` source attribute.

#### Group Membership Configuration

Expand Down
6 changes: 6 additions & 0 deletions forge/db/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ module.exports = {
[Op.or]: [{ invitorId: user.id }, { inviteeId: user.id }]
}
})
await M.AccessToken.destroy({
where: {
ownerType: 'user',
ownerId: '' + user.id
}
})
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion forge/services/snapshots.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ module.exports.copySnapshot = async (
await app.db.controllers.Project.importProjectSnapshot(
toInstance,
newSnapshot,
{ mergeEnvVars: true, mergeEditorSettings: false }
{ mergeEnvVars: true, mergeEditorSettings: true }
)
}

Expand Down
38 changes: 20 additions & 18 deletions frontend/src/pages/admin/Template/sections/Security.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,28 @@
<LockSetting v-model="editable.policy.httpNodeAuth_type" class="flex justify-end flex-col" :editTemplate="editTemplate" :changed="editable.changed.policy.httpNodeAuth_type" />
</div>
<ff-radio-group v-model="editable.settings.httpNodeAuth_type" orientation="vertical" :options="authOptions1" />
<div class="flex flex-col sm:flex-row sm:ml-4">
<div class="space-y-4 w-full max-w-md sm:mr-8">
<FormRow v-model="editable.settings.httpNodeAuth_user" :disabled="editable.settings.httpNodeAuth_type !=='basic' || !editTemplate && !editable.policy.httpNodeAuth_user" :type="(editTemplate||editable.policy.httpNodeAuth_user)?'text':'uneditable'">
HTTP Auth Username
<template #append><ChangeIndicator :value="editable.changed.settings.httpNodeAuth_user" /></template>
</FormRow>
<template v-if="editable.settings?.httpNodeAuth_type === 'basic'">
<div class="flex flex-col sm:flex-row sm:ml-4">
<div class="space-y-4 w-full max-w-md sm:mr-8">
<FormRow v-model="editable.settings.httpNodeAuth_user" :disabled="editable.settings.httpNodeAuth_type !=='basic' || !editTemplate && !editable.policy.httpNodeAuth_user" :type="(editTemplate||editable.policy.httpNodeAuth_user)?'text':'uneditable'">
HTTP Auth Username
<template #append><ChangeIndicator :value="editable.changed.settings.httpNodeAuth_user" /></template>
</FormRow>
</div>
<LockSetting v-model="editable.policy.httpNodeAuth_user" class="flex justify-end flex-col" :editTemplate="editTemplate" :changed="editable.changed.policy.httpNodeAuth_user" />
</div>
<LockSetting v-model="editable.policy.httpNodeAuth_user" class="flex justify-end flex-col" :editTemplate="editTemplate" :changed="editable.changed.policy.httpNodeAuth_user" />
</div>
<div class="flex flex-col sm:flex-row sm:ml-4">
<div class="space-y-4 w-full max-w-md sm:mr-8">
<FormRow v-model="editable.settings.httpNodeAuth_pass" :disabled="editable.settings.httpNodeAuth_type !=='basic' || !editTemplate && !editable.policy.httpNodeAuth_pass" :type="(editTemplate||editable.policy.httpNodeAuth_pass)?'password':'uneditable'">
HTTP Auth Password
<template #append><ChangeIndicator :value="editable.changed.settings.httpNodeAuth_pass" /></template>
</FormRow>
<div class="flex flex-col sm:flex-row sm:ml-4">
<div class="space-y-4 w-full max-w-md sm:mr-8">
<FormRow v-model="editable.settings.httpNodeAuth_pass" :disabled="editable.settings.httpNodeAuth_type !=='basic' || !editTemplate && !editable.policy.httpNodeAuth_pass" :type="(editTemplate||editable.policy.httpNodeAuth_pass)?'password':'uneditable'">
HTTP Auth Password
<template #append><ChangeIndicator :value="editable.changed.settings.httpNodeAuth_pass" /></template>
</FormRow>
</div>
<LockSetting v-model="editable.policy.httpNodeAuth_pass" class="flex justify-end flex-col" :editTemplate="editTemplate" :changed="editable.changed.policy.httpNodeAuth_pass" />
</div>
<LockSetting v-model="editable.policy.httpNodeAuth_pass" class="flex justify-end flex-col" :editTemplate="editTemplate" :changed="editable.changed.policy.httpNodeAuth_pass" />
</div>
</template>
<FeatureUnavailableToTeam v-if="!ffAuthFeatureAvailable" featureName="FlowFuse User Authentication" />
<ff-radio-group v-model="editable.settings.httpNodeAuth_type" orientation="vertical" :options="authOptions2" />
<ff-radio-group v-model="editable.settings.httpNodeAuth_type" data-el="http-auth-option-ff" orientation="vertical" :options="authOptions2" />
</form>
</template>

Expand Down Expand Up @@ -104,7 +106,7 @@ export default {
label: 'FlowFuse User Authentication',
value: 'flowforge-user',
disabled: !this.ffAuthFeatureAvailable || (!this.editTemplate && !this.editable.policy.httpNodeAuth_type),
description: 'Only members of the application instance\'s team will be able to access the routes'
description: 'Only members of the instance\'s team will be able to access the routes'
}
]
}
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/pages/application/Overview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
</template>
<template #tools>
<ff-button
v-if="hasPermission('project:create')"
data-action="create-instance"
:to="{ name: 'ApplicationCreateInstance' }"
>
Expand All @@ -30,7 +31,7 @@
@row-selected="selectedCloudRow"
>
<template
v-if="hasPermission('device:edit')"
v-if="hasPermission('project:change-status')"
#context-menu="{row}"
>
<ff-list-item
Expand Down Expand Up @@ -72,6 +73,7 @@
</template>
<template #actions>
<ff-button
v-if="hasPermission('project:create')"
:to="{ name: 'ApplicationCreateInstance' }"
>
<template #icon-left><PlusSmIcon /></template>
Expand Down
50 changes: 28 additions & 22 deletions frontend/src/pages/application/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,41 @@
Description
</FormRow>
</div>
<div class="space-x-4 whitespace-nowrap">
<template v-if="!editing">
<ff-button kind="primary" data-action="application-edit" @click="editName">Edit</ff-button>
</template>
<template v-else>
<div class="flex gap-x-3">
<ff-button kind="secondary" @click="cancelEditName">Cancel</ff-button>
<ff-button kind="primary" :disabled="!formValid" data-form="submit" @click="saveApplication">Save</ff-button>
<template v-if="hasPermission('project:edit')">
<div class="space-x-4 whitespace-nowrap">
<template v-if="!editing">
<ff-button kind="primary" data-action="application-edit" @click="editName">Edit</ff-button>
</template>
<template v-else>
<div class="flex gap-x-3">
<ff-button kind="secondary" @click="cancelEditName">Cancel</ff-button>
<ff-button kind="primary" :disabled="!formValid" data-form="submit" @click="saveApplication">Save</ff-button>
</div>
</template>
</div>
</template>
<template v-if="hasPermission('project:delete')">
<FormHeading class="text-red-700">Delete Application</FormHeading>
<div class="flex flex-col space-y-4 max-w-2xl">
<div class="flex-grow">
<div class="max-w-sm">
{{ getDeleteApplicationText }}
</div>
</div>
</template>
</div>

<FormHeading class="text-red-700">Delete Application</FormHeading>
<div class="flex flex-col space-y-4 max-w-2xl">
<div class="flex-grow">
<div class="max-w-sm">
{{ getDeleteApplicationText }}
<div class="min-w-fit flex-shrink-0">
<ff-button data-action="delete-application" kind="danger" :disabled="options.instances > 0" @click="$emit('application-delete')">
Delete Application
</ff-button>
</div>
</div>
<div class="min-w-fit flex-shrink-0">
<ff-button data-action="delete-application" kind="danger" :disabled="options.instances > 0" @click="$emit('application-delete')">
Delete Application
</ff-button>
</div>
</div>
</template>
</div>
</div>
</template>

<script>
import { mapState } from 'vuex'
import ApplicationAPI from '../../api/application.js'
import FormHeading from '../../components/FormHeading.vue'
Expand Down Expand Up @@ -93,6 +98,7 @@ export default {
}
},
computed: {
...mapState('account', ['teamMembership']),
formValid () {
return this.input.projectName
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('FlowFuse EE - HTTP Auth tokens', () => {
navigateToInstanceSettings('BTeam', 'instance-2-1')

cy.get('[data-nav="security"').click()
cy.get('[data-el="http-auth"] div:nth-child(6)').click()
cy.get('[data-el="http-auth-option-ff"]').click()

cy.get('[data-action="new-token"]').should('exist')
cy.get('[data-action="new-token"] span:first').click()
Expand Down
4 changes: 4 additions & 0 deletions test/unit/forge/ee/routes/api/pipeline_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,10 @@ describe('Pipelines API', function () {
targetSnapshot.settings.env.should.have.property('two', 'b')
targetSnapshot.settings.should.have.property('modules')

const instanceSettings = await TestObjects.instanceTwo.getSetting('settings')
instanceSettings.should.have.property('header')
instanceSettings.header.should.have.property('title', 'instance-two')

// Verify the container driver was asked to restart the flows
app.log.info.calledWith(`[stub driver] Restarting flows ${TestObjects.instanceTwo.id}`).should.be.true()
})
Expand Down
23 changes: 23 additions & 0 deletions test/unit/forge/routes/api/user_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,29 @@ describe('User API', async function () {
})
deleteResponse.statusCode.should.equal(404)
})
it('Deleting a user removes any PATs from the db', async function () {
const userToDelete = await app.db.models.User.create({ username: 'wayne', name: 'Wayne Vane', email: 'wayne@example.com', email_verified: true, password: 'wwPassword' })
await login('wayne', 'wwPassword')
const response = await app.inject({
method: 'POST',
url: '/api/v1/user/tokens',
cookies: { sid: TestObjects.tokens.wayne },
payload: {
name: 'Waynes Token',
scope: ''
}
})
response.statusCode.should.equal(200)

const userId = userToDelete.id
const tokens = await app.db.models.AccessToken.getPersonalAccessTokens({ id: userId })
tokens.should.have.length(1)

await userToDelete.destroy()

const tokens2 = await app.db.models.AccessToken.getPersonalAccessTokens({ id: userId })
tokens2.should.have.length(0)
})
})

describe('User invites', async function () {
Expand Down

0 comments on commit ff17652

Please sign in to comment.