mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-03 20:40:00 -05:00
feat: Add Microsoft Intune MAM authentication support (#34577)
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Waiting to run
Web App CI / check-types (push) Waiting to run
Web App CI / test (push) Waiting to run
Web App CI / build (push) Waiting to run
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Waiting to run
Web App CI / check-types (push) Waiting to run
Web App CI / test (push) Waiting to run
Web App CI / build (push) Waiting to run
* Add Entra ID token authentication and Intune MAM config exposure * Add Intune MAM toggle to Mobile Security admin console * Add IntuneSettings with the AuthService to use and its own TenantID andClientID for the Entra App registration Include Admin console changes switch from /oauth/entra to /oauth/intune endpoint * openAPI documentation --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: yasser khan <attitude3cena.yf@gmail.com>
This commit is contained in:
parent
dc881dc470
commit
4589005a54
29 changed files with 2389 additions and 266 deletions
|
|
@ -2375,6 +2375,18 @@ components:
|
|||
private_key_file:
|
||||
description: Status is good when `true`
|
||||
type: boolean
|
||||
IntuneLoginRequest:
|
||||
type: object
|
||||
description: Request body for Microsoft Intune MAM authentication using Azure AD/Entra ID access token
|
||||
required:
|
||||
- access_token
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
description: Microsoft Entra ID access token obtained via MSAL (Microsoft Authentication Library). This token must be scoped to the Intune MAM app registration and will be validated against the configured tenant.
|
||||
device_id:
|
||||
type: string
|
||||
description: Optional mobile device identifier used for push notifications. If provided, the device will be registered for receiving push notifications.
|
||||
Compliance:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
|
|
@ -127,6 +127,145 @@
|
|||
$ref: "#/components/responses/BadRequest"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
/oauth/intune:
|
||||
post:
|
||||
tags:
|
||||
- users
|
||||
summary: Login with Microsoft Intune MAM
|
||||
description: >
|
||||
Authenticate a mobile user using a Microsoft Entra ID (Azure AD) access token
|
||||
for Intune Mobile Application Management (MAM) protected apps.
|
||||
|
||||
|
||||
This endpoint enables authentication for mobile apps protected by Microsoft Intune MAM
|
||||
policies. The access token is obtained via the Microsoft Authentication Library (MSAL)
|
||||
and validated against the configured Azure AD tenant and Intune MAM app registration.
|
||||
|
||||
|
||||
**Authentication Flow:**
|
||||
|
||||
1. Mobile app acquires an Entra ID access token via MSAL with the Intune MAM scope
|
||||
|
||||
2. Token is sent to this endpoint for validation
|
||||
|
||||
3. Server validates the token signature, claims, and tenant configuration
|
||||
|
||||
4. User is authenticated or created based on the token claims
|
||||
|
||||
5. Session token is returned for subsequent API requests
|
||||
|
||||
|
||||
**User Provisioning:**
|
||||
|
||||
- **Office365 AuthService**: Users are automatically created on first login using
|
||||
the `oid` (Azure AD object ID) claim as the unique identifier
|
||||
|
||||
- **SAML AuthService**: Users must first login via web/desktop to establish their
|
||||
account with the `oid` (Azure AD object ID) as AuthData. Intune MAM
|
||||
always uses objectId for SAML users. For Entra ID Domain Services LDAP sync,
|
||||
configure LdapSettings.IdAttribute to `msDS-aadObjectId` to ensure consistency.
|
||||
|
||||
|
||||
**Error Handling:**
|
||||
|
||||
This endpoint returns specific HTTP status codes to help mobile apps handle different
|
||||
error scenarios:
|
||||
|
||||
- `428 Precondition Required`: SAML user needs to login via web/desktop first
|
||||
|
||||
- `403 Forbidden`: Configuration issues or bot accounts
|
||||
|
||||
- `409 Conflict`: User account is deactivated
|
||||
|
||||
- `401 Unauthorized`: Token has expired
|
||||
|
||||
- `400 Bad Request`: Invalid token format, claims, or configuration
|
||||
|
||||
|
||||
##### Permissions
|
||||
|
||||
|
||||
No permission required. Authentication is performed via the Entra ID access token.
|
||||
|
||||
|
||||
##### Enterprise Feature
|
||||
|
||||
|
||||
Requires Mattermost Enterprise Advanced license and proper Intune MAM configuration
|
||||
(tenant ID, client ID, and auth service).
|
||||
operationId: LoginIntune
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/IntuneLoginRequest"
|
||||
description: Intune login credentials containing the Entra ID access token
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: User authentication successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/User"
|
||||
"400":
|
||||
description: >
|
||||
Bad request - Invalid token format, signature, claims, or configuration.
|
||||
Common causes include: invalid JSON body, missing access_token, malformed JWT,
|
||||
invalid token issuer/audience/tenant, missing required claims (oid, email),
|
||||
or empty auth data after extraction.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AppError"
|
||||
"401":
|
||||
description: Unauthorized - The Entra ID access token has expired
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AppError"
|
||||
"403":
|
||||
description: >
|
||||
Forbidden - Access denied. Common causes include: Intune MAM not properly
|
||||
configured or enabled, or user is a bot account (bots cannot use Intune login).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AppError"
|
||||
"409":
|
||||
description: Conflict - User account has been deactivated (DeleteAt != 0)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AppError"
|
||||
"428":
|
||||
description: >
|
||||
Precondition Required - SAML user account not found. The user must first
|
||||
login via web or desktop application to establish their Mattermost account
|
||||
with objectId as AuthData before using mobile Intune MAM authentication.
|
||||
For Entra ID Domain Services LDAP sync, ensure SamlSettings.IdAttribute references
|
||||
the objectidentifier claim and LdapSettings.IdAttribute is set to 'msDS-aadObjectId'.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AppError"
|
||||
"500":
|
||||
description: >
|
||||
Internal Server Error - Server-side error. Common causes include: failed to
|
||||
initialize JWKS (JSON Web Key Set) from Microsoft's OpenID configuration,
|
||||
or failed to create user session.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AppError"
|
||||
"501":
|
||||
description: >
|
||||
Not Implemented - Intune MAM feature is not available. This occurs when
|
||||
running Mattermost Team Edition or when enterprise features are not loaded.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AppError"
|
||||
/api/v4/users/logout:
|
||||
post:
|
||||
tags:
|
||||
|
|
|
|||
|
|
@ -561,6 +561,13 @@ const defaultServerConfig: AdminConfig = {
|
|||
MobileJailbreakProtection: false,
|
||||
MobileEnableSecureFilePreview: false,
|
||||
MobileAllowPdfLinkNavigation: false,
|
||||
EnableIntuneMAM: false,
|
||||
},
|
||||
IntuneSettings: {
|
||||
Enable: false,
|
||||
TenantId: '',
|
||||
ClientId: '',
|
||||
AuthService: '',
|
||||
},
|
||||
CacheSettings: {
|
||||
CacheType: 'lru',
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {expect, Locator, Page} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* System Console -> Environment -> Mobile Security
|
||||
*/
|
||||
export default class MobileSecurity {
|
||||
readonly page: Page;
|
||||
readonly container: Locator;
|
||||
|
||||
readonly enableBiometricAuthenticationToggleTrue: Locator;
|
||||
|
|
@ -19,11 +20,22 @@ export default class MobileSecurity {
|
|||
readonly enableSecureFilePreviewToggleFalse: Locator;
|
||||
readonly allowPdfLinkNavigationToggleTrue: Locator;
|
||||
readonly allowPdfLinkNavigationToggleFalse: Locator;
|
||||
readonly enableIntuneMAMToggleTrue: Locator;
|
||||
readonly enableIntuneMAMToggleFalse: Locator;
|
||||
|
||||
// New IntuneSettings fields
|
||||
readonly enableIntuneToggleTrue: Locator;
|
||||
readonly enableIntuneToggleFalse: Locator;
|
||||
readonly intuneAuthServiceDropdown: Locator;
|
||||
readonly intuneTenantIdInput: Locator;
|
||||
readonly intuneClientIdInput: Locator;
|
||||
readonly intuneTenantIdRequiredError: Locator;
|
||||
|
||||
readonly saveButton: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
constructor(container: Locator, page: Page) {
|
||||
this.container = container;
|
||||
this.page = page;
|
||||
|
||||
this.enableBiometricAuthenticationToggleTrue = this.container.getByTestId(
|
||||
'NativeAppSettings.MobileEnableBiometricstrue',
|
||||
|
|
@ -67,9 +79,29 @@ export default class MobileSecurity {
|
|||
'NativeAppSettings.MobileAllowPdfLinkNavigationfalse',
|
||||
);
|
||||
|
||||
// Legacy Intune toggle (will be removed in Phase 6)
|
||||
this.enableIntuneMAMToggleTrue = this.container.getByTestId('IntuneSettings.Enabletrue');
|
||||
this.enableIntuneMAMToggleFalse = this.container.getByTestId('IntuneSettings.Enablefalse');
|
||||
|
||||
// New IntuneSettings fields
|
||||
this.enableIntuneToggleTrue = this.container.getByTestId('IntuneSettings.Enabletrue');
|
||||
this.enableIntuneToggleFalse = this.container.getByTestId('IntuneSettings.Enablefalse');
|
||||
this.intuneAuthServiceDropdown = this.container.getByTestId('IntuneSettings.AuthServicedropdown');
|
||||
this.intuneTenantIdInput = this.container.getByTestId('IntuneSettings.TenantIdinput');
|
||||
this.intuneClientIdInput = this.container.getByTestId('IntuneSettings.ClientIdinput');
|
||||
this.intuneTenantIdRequiredError = this.container.getByTestId('errorMessage');
|
||||
|
||||
this.saveButton = this.container.getByRole('button', {name: 'Save'});
|
||||
}
|
||||
|
||||
async discardChanges() {
|
||||
this.page.getByRole('button', {name: 'Yes, Discard'}).click();
|
||||
}
|
||||
|
||||
async intuneTenantIdRequiredErrorToBeVisible() {
|
||||
await expect(this.intuneTenantIdRequiredError).toBeVisible();
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
|
@ -114,6 +146,39 @@ export default class MobileSecurity {
|
|||
await this.allowPdfLinkNavigationToggleFalse.click();
|
||||
}
|
||||
|
||||
async clickEnableIntuneMAMToggleTrue() {
|
||||
await this.enableIntuneMAMToggleTrue.click();
|
||||
}
|
||||
|
||||
async selectAuthProvider(value: 'office365' | 'saml') {
|
||||
await this.intuneAuthServiceDropdown.selectOption(value);
|
||||
}
|
||||
|
||||
async clickEnableIntuneMAMToggleFalse() {
|
||||
await this.enableIntuneMAMToggleFalse.click();
|
||||
}
|
||||
|
||||
// New IntuneSettings methods
|
||||
async clickEnableIntuneToggleTrue() {
|
||||
await this.enableIntuneToggleTrue.click();
|
||||
}
|
||||
|
||||
async clickEnableIntuneToggleFalse() {
|
||||
await this.enableIntuneToggleFalse.click();
|
||||
}
|
||||
|
||||
async selectIntuneAuthService(value: 'office365' | 'saml') {
|
||||
await this.intuneAuthServiceDropdown.selectOption(value);
|
||||
}
|
||||
|
||||
async fillIntuneTenantId(value: string) {
|
||||
await this.intuneTenantIdInput.fill(value);
|
||||
}
|
||||
|
||||
async fillIntuneClientId(value: string) {
|
||||
await this.intuneClientIdInput.fill(value);
|
||||
}
|
||||
|
||||
async clickSaveButton() {
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export default class SystemConsolePage {
|
|||
this.systemUsers = new components.SystemUsers(page.getByTestId('systemUsersSection'));
|
||||
this.mobileSecurity = new components.SystemConsoleMobileSecurity(
|
||||
page.getByTestId('sysconsole_section_MobileSecuritySettings'),
|
||||
this.page,
|
||||
);
|
||||
this.featureDiscovery = new components.SystemConsoleFeatureDiscovery(page.getByTestId('featureDiscovery'));
|
||||
|
||||
|
|
|
|||
|
|
@ -185,3 +185,310 @@ test('should show mobile security upsell when not licensed', async ({pw}) => {
|
|||
// * Verify title is correct
|
||||
await systemConsolePage.featureDiscovery.toHaveTitle('Enhance mobile app security with Mattermost Enterprise');
|
||||
});
|
||||
|
||||
test('should show and enable Intune MAM when Enterprise Advanced licensed and Office365 configured', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
test.skip(license.SkuShortName !== 'advanced', 'Skipping test - server does not have enterprise advanced license');
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Configure Office365 settings
|
||||
const config = await adminClient.getConfig();
|
||||
config.Office365Settings.Enable = true;
|
||||
config.Office365Settings.Id = 'test-client-id';
|
||||
config.Office365Settings.Secret = 'test-client-secret';
|
||||
config.Office365Settings.DirectoryId = 'test-directory-id';
|
||||
await adminClient.updateConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Go to Mobile Security section
|
||||
await systemConsolePage.sidebar.goToItem('Mobile Security');
|
||||
|
||||
// * Verify Intune MAM toggle is visible
|
||||
await expect(systemConsolePage.mobileSecurity.enableIntuneMAMToggleTrue).toBeVisible();
|
||||
await expect(systemConsolePage.mobileSecurity.enableIntuneMAMToggleFalse).toBeVisible();
|
||||
|
||||
// # Enable Intune MAM
|
||||
await systemConsolePage.mobileSecurity.clickEnableIntuneMAMToggleTrue();
|
||||
|
||||
// * Verify Intune MAM is enabled
|
||||
await expect(await systemConsolePage.mobileSecurity.enableIntuneMAMToggleTrue.isChecked()).toBe(true);
|
||||
|
||||
await systemConsolePage.mobileSecurity.selectIntuneAuthService('office365');
|
||||
|
||||
// # Fill in Intune configuration
|
||||
await systemConsolePage.mobileSecurity.fillIntuneTenantId('12345678-1234-1234-1234-123456789012');
|
||||
await systemConsolePage.mobileSecurity.fillIntuneClientId('87654321-4321-4321-4321-210987654321');
|
||||
|
||||
// # Save settings
|
||||
await systemConsolePage.mobileSecurity.clickSaveButton();
|
||||
|
||||
// # Wait until the save button has settled
|
||||
await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save');
|
||||
|
||||
// # Go to any other section and come back to Mobile Security
|
||||
await systemConsolePage.sidebar.goToItem('Users');
|
||||
await systemConsolePage.systemUsers.toBeVisible();
|
||||
|
||||
await systemConsolePage.sidebar.goToItem('Mobile Security');
|
||||
|
||||
// * Verify Intune MAM is still enabled
|
||||
await expect(await systemConsolePage.mobileSecurity.enableIntuneMAMToggleTrue.isChecked()).toBe(true);
|
||||
});
|
||||
|
||||
test('should hide Intune MAM when Office365 is not configured', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
test.skip(license.SkuShortName !== 'advanced', 'Skipping test - server does not have enterprise advanced license');
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Ensure Office365 is disabled
|
||||
const config = await adminClient.getConfig();
|
||||
config.Office365Settings.Enable = false;
|
||||
await adminClient.updateConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Go to Mobile Security section
|
||||
await systemConsolePage.sidebar.goToItem('Mobile Security');
|
||||
|
||||
// * Verify Intune MAM toggle is visible
|
||||
await expect(systemConsolePage.mobileSecurity.enableIntuneMAMToggleTrue).toBeVisible();
|
||||
await expect(systemConsolePage.mobileSecurity.enableIntuneMAMToggleFalse).toBeVisible();
|
||||
await expect(systemConsolePage.mobileSecurity.intuneAuthServiceDropdown).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should configure new IntuneSettings with Office365 auth provider', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
test.skip(license.SkuShortName !== 'advanced', 'Skipping test - server does not have enterprise advanced license');
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Configure Office365 settings
|
||||
const config = await adminClient.getConfig();
|
||||
config.Office365Settings.Enable = true;
|
||||
config.Office365Settings.Id = 'test-office365-client-id';
|
||||
config.Office365Settings.Secret = 'test-office365-secret';
|
||||
config.Office365Settings.DirectoryId = 'test-office365-directory-id';
|
||||
config.SamlSettings.EmailAttribute = 'useremail';
|
||||
await adminClient.updateConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Go to Mobile Security section
|
||||
await systemConsolePage.sidebar.goToItem('Mobile Security');
|
||||
|
||||
// * Verify new Intune toggle is visible
|
||||
await expect(systemConsolePage.mobileSecurity.enableIntuneToggleTrue).toBeVisible();
|
||||
await expect(systemConsolePage.mobileSecurity.enableIntuneToggleFalse).toBeVisible();
|
||||
|
||||
// # Enable Intune
|
||||
await systemConsolePage.mobileSecurity.clickEnableIntuneToggleTrue();
|
||||
|
||||
// * Verify Intune is enabled
|
||||
expect(await systemConsolePage.mobileSecurity.enableIntuneToggleTrue.isChecked()).toBe(true);
|
||||
|
||||
// # Select Office365 as auth provider
|
||||
await systemConsolePage.mobileSecurity.selectIntuneAuthService('office365');
|
||||
|
||||
// # Fill in Intune configuration
|
||||
await systemConsolePage.mobileSecurity.fillIntuneTenantId('12345678-1234-1234-1234-123456789012');
|
||||
await systemConsolePage.mobileSecurity.fillIntuneClientId('87654321-4321-4321-4321-210987654321');
|
||||
|
||||
// # Save settings
|
||||
await systemConsolePage.mobileSecurity.clickSaveButton();
|
||||
|
||||
// # Wait until the save button has settled
|
||||
await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save');
|
||||
|
||||
// # Go to any other section and come back to Mobile Security
|
||||
await systemConsolePage.sidebar.goToItem('Users');
|
||||
await systemConsolePage.systemUsers.toBeVisible();
|
||||
|
||||
await systemConsolePage.sidebar.goToItem('Mobile Security');
|
||||
|
||||
// * Verify Intune is still enabled and configured
|
||||
expect(await systemConsolePage.mobileSecurity.enableIntuneToggleTrue.isChecked()).toBe(true);
|
||||
expect(await systemConsolePage.mobileSecurity.intuneTenantIdInput.inputValue()).toBe(
|
||||
'12345678-1234-1234-1234-123456789012',
|
||||
);
|
||||
expect(await systemConsolePage.mobileSecurity.intuneClientIdInput.inputValue()).toBe(
|
||||
'87654321-4321-4321-4321-210987654321',
|
||||
);
|
||||
});
|
||||
|
||||
test('should configure new IntuneSettings with SAML auth provider', async ({pw}) => {
|
||||
// # Configure SAML settings
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const config = await adminClient.getConfig();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
test.skip(license.SkuShortName !== 'advanced', 'Skipping test - server does not have enterprise advanced license');
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Set server URL for fetch calls
|
||||
const serverUrl = process.env.MM_SERVER_URL || 'http://localhost:8065';
|
||||
|
||||
// # Upload a valid SAML IdP certificate using fetch
|
||||
const idpCert = `-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAKC1r6Qw3v6OMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAlVTMRYwFAYDVQQIDA1Tb21lLVN0YXRlMRYwFAYDVQQKDA1FeGFtcGxlIEluYy4wHhcNMTkwMTAxMDAwMDAwWhcNMjkwMTAxMDAwMDAwWjBFMQswCQYDVQQGEwJVUzEWMBQGA1UECAwNU29tZS1TdGF0ZTEWMBQGA1UECgwNRXhhbXBsZSBJbmMuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6QwIDAQABo1AwTjAdBgNVHQ4EFgQU6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAU6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKQw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw=\n-----END CERTIFICATE-----\n`;
|
||||
const idpFormData = new FormData();
|
||||
idpFormData.append(
|
||||
'certificate',
|
||||
new Blob([idpCert], {type: 'application/x-x509-ca-cert'}),
|
||||
'Intune SAML Test.cer',
|
||||
);
|
||||
await fetch(`${serverUrl}/api/v4/saml/certificate/idp`, {
|
||||
method: 'POST',
|
||||
body: idpFormData,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
// # Upload a minimal, valid SP public certificate (PEM-encoded)
|
||||
const spCert = `-----BEGIN CERTIFICATE-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArv1Qw4v7OMC2r7Qw4v7OMC2r7Qw4v7OMC2r7QwIDAQAB\n-----END CERTIFICATE-----\n`;
|
||||
const spFormData = new FormData();
|
||||
spFormData.append('certificate', new Blob([spCert], {type: 'application/x-x509-ca-cert'}), 'saml-public-cert.pem');
|
||||
await fetch(`${serverUrl}/api/v4/saml/certificate/public`, {
|
||||
method: 'POST',
|
||||
body: spFormData,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
// # Configure SAML settings
|
||||
config.SamlSettings.Enable = true;
|
||||
config.SamlSettings.IdpURL = 'https://example.com/saml';
|
||||
config.SamlSettings.IdpDescriptorURL = 'https://example.com/saml/metadata';
|
||||
config.SamlSettings.IdpCertificateFile = 'test-cert.pem';
|
||||
config.SamlSettings.EmailAttribute = 'useremail';
|
||||
config.SamlSettings.UsernameAttribute = 'username';
|
||||
config.SamlSettings.ServiceProviderIdentifier = 'sp-entity-id';
|
||||
config.SamlSettings.AssertionConsumerServiceURL = 'https://sp.example.com/login';
|
||||
config.SamlSettings.IdpCertificateFile = 'saml-idp.crt';
|
||||
config.SamlSettings.PrivateKeyFile = 'saml-idp.crt';
|
||||
|
||||
if ('PublicCertificateFile' in config.SamlSettings) {
|
||||
config.SamlSettings.PublicCertificateFile = 'saml-public-cert.pem';
|
||||
}
|
||||
await adminClient.updateConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Go to Mobile Security section
|
||||
await systemConsolePage.sidebar.goToItem('Mobile Security');
|
||||
|
||||
// * Verify new Intune toggle is visible
|
||||
await expect(systemConsolePage.mobileSecurity.enableIntuneToggleTrue).toBeVisible();
|
||||
|
||||
// # Enable Intune
|
||||
await systemConsolePage.mobileSecurity.clickEnableIntuneToggleTrue();
|
||||
|
||||
// # Select SAML as auth provider
|
||||
await systemConsolePage.mobileSecurity.selectIntuneAuthService('saml');
|
||||
|
||||
// # Fill in Intune configuration
|
||||
await systemConsolePage.mobileSecurity.fillIntuneTenantId('abcdef01-2345-6789-abcd-ef0123456789');
|
||||
await systemConsolePage.mobileSecurity.fillIntuneClientId('fedcba98-7654-3210-fedc-ba9876543210');
|
||||
|
||||
// # Save settings
|
||||
await systemConsolePage.mobileSecurity.clickSaveButton();
|
||||
|
||||
// # Wait until the save button has settled
|
||||
await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save');
|
||||
|
||||
// # Go to any other section and come back to Mobile Security
|
||||
await systemConsolePage.sidebar.goToItem('Users');
|
||||
await systemConsolePage.systemUsers.toBeVisible();
|
||||
|
||||
await systemConsolePage.sidebar.goToItem('Mobile Security');
|
||||
|
||||
// * Verify Intune is still enabled and configured with SAML
|
||||
expect(await systemConsolePage.mobileSecurity.enableIntuneToggleTrue.isChecked()).toBe(true);
|
||||
expect(await systemConsolePage.mobileSecurity.intuneTenantIdInput.inputValue()).toBe(
|
||||
'abcdef01-2345-6789-abcd-ef0123456789',
|
||||
);
|
||||
expect(await systemConsolePage.mobileSecurity.intuneClientIdInput.inputValue()).toBe(
|
||||
'fedcba98-7654-3210-fedc-ba9876543210',
|
||||
);
|
||||
});
|
||||
|
||||
test('should disable Intune inputs when toggle is off', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
test.skip(license.SkuShortName !== 'advanced', 'Skipping test - server does not have enterprise advanced license');
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Configure Office365 settings
|
||||
const config = await adminClient.getConfig();
|
||||
config.Office365Settings.Enable = true;
|
||||
config.Office365Settings.Id = 'test-client-id';
|
||||
config.Office365Settings.Secret = 'test-secret';
|
||||
config.Office365Settings.DirectoryId = 'test-directory-id';
|
||||
await adminClient.updateConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Go to Mobile Security section
|
||||
await systemConsolePage.sidebar.goToItem('Mobile Security');
|
||||
|
||||
// * Verify Intune inputs are disabled when toggle is off
|
||||
expect(await systemConsolePage.mobileSecurity.intuneAuthServiceDropdown.isDisabled()).toBe(true);
|
||||
expect(await systemConsolePage.mobileSecurity.intuneTenantIdInput.isDisabled()).toBe(true);
|
||||
expect(await systemConsolePage.mobileSecurity.intuneClientIdInput.isDisabled()).toBe(true);
|
||||
|
||||
// # Enable Intune
|
||||
await systemConsolePage.mobileSecurity.clickEnableIntuneToggleTrue();
|
||||
|
||||
// * Verify Intune inputs are now enabled
|
||||
expect(await systemConsolePage.mobileSecurity.intuneAuthServiceDropdown.isDisabled()).toBe(false);
|
||||
expect(await systemConsolePage.mobileSecurity.intuneTenantIdInput.isDisabled()).toBe(false);
|
||||
expect(await systemConsolePage.mobileSecurity.intuneClientIdInput.isDisabled()).toBe(false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ func (a *App) Notification() einterfaces.NotificationInterface {
|
|||
func (a *App) Saml() einterfaces.SamlInterface {
|
||||
return a.ch.Saml
|
||||
}
|
||||
func (a *App) Intune() einterfaces.IntuneInterface {
|
||||
return a.ch.Intune
|
||||
}
|
||||
func (a *App) Cloud() einterfaces.CloudInterface {
|
||||
return a.ch.srv.Cloud
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ type Channels struct {
|
|||
Notification einterfaces.NotificationInterface
|
||||
Ldap einterfaces.LdapInterface
|
||||
AccessControl einterfaces.AccessControlServiceInterface
|
||||
Intune einterfaces.IntuneInterface
|
||||
|
||||
// These are used to prevent concurrent upload requests
|
||||
// for a given upload session which could cause inconsistencies
|
||||
|
|
@ -135,6 +136,10 @@ func NewChannels(s *Server) (*Channels, error) {
|
|||
})
|
||||
}
|
||||
|
||||
if intuneInterface != nil {
|
||||
ch.Intune = intuneInterface(New(ServerConnector(ch)))
|
||||
}
|
||||
|
||||
if pushProxyInterface != nil {
|
||||
app := New(ServerConnector(ch))
|
||||
s.PushProxy = pushProxyInterface(app)
|
||||
|
|
|
|||
|
|
@ -116,6 +116,12 @@ func RegisterPushProxyInterface(f func(*App) einterfaces.PushProxyInterface) {
|
|||
pushProxyInterface = f
|
||||
}
|
||||
|
||||
var intuneInterface func(*App) einterfaces.IntuneInterface
|
||||
|
||||
func RegisterIntuneInterface(f func(*App) einterfaces.IntuneInterface) {
|
||||
intuneInterface = f
|
||||
}
|
||||
|
||||
func (s *Server) initEnterprise() {
|
||||
if cloudInterface != nil {
|
||||
s.Cloud = cloudInterface(s)
|
||||
|
|
|
|||
|
|
@ -794,6 +794,62 @@ func (a *App) LoginByOAuth(rctx request.CTX, service string, userData io.Reader,
|
|||
return user, nil
|
||||
}
|
||||
|
||||
// LoginByIntune authenticates a user using a Microsoft Entra ID access_token from MSAL
|
||||
// This is used by mobile clients with Microsoft Intune MAM enabled
|
||||
func (a *App) LoginByIntune(rctx request.CTX, accessToken string) (*model.User, *model.AppError) {
|
||||
// Check if Intune interface is available (enterprise feature)
|
||||
if a.Intune() == nil {
|
||||
return nil, model.NewAppError(
|
||||
"App.LoginByIntune",
|
||||
"api.user.login_by_intune.not_available.app_error",
|
||||
nil,
|
||||
"",
|
||||
http.StatusNotImplemented,
|
||||
)
|
||||
}
|
||||
|
||||
// Check if Intune is configured
|
||||
if !a.Intune().IsConfigured() {
|
||||
return nil, model.NewAppError(
|
||||
"App.LoginByIntune",
|
||||
"api.user.login_by_intune.not_configured.app_error",
|
||||
nil,
|
||||
"",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
}
|
||||
|
||||
// Perform Intune login via enterprise interface
|
||||
user, appErr := a.Intune().Login(rctx, accessToken)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
// Prevent bot login
|
||||
if user.IsBot {
|
||||
return nil, model.NewAppError(
|
||||
"App.LoginByIntune",
|
||||
"api.user.login_by_intune.bot_login_forbidden.app_error",
|
||||
nil,
|
||||
"",
|
||||
http.StatusForbidden,
|
||||
)
|
||||
}
|
||||
|
||||
// Check if account is locked/disabled
|
||||
if user.DeleteAt != 0 {
|
||||
return nil, model.NewAppError(
|
||||
"App.LoginByIntune",
|
||||
"api.user.login_by_intune.account_locked.app_error",
|
||||
nil,
|
||||
"user_id="+user.Id,
|
||||
http.StatusConflict,
|
||||
)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *App) CompleteSwitchWithOAuth(rctx request.CTX, service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) {
|
||||
provider, e := a.getSSOProvider(service)
|
||||
if e != nil {
|
||||
|
|
|
|||
|
|
@ -1586,3 +1586,236 @@ func TestAuthorizeOAuthUser_InvalidToken(t *testing.T) {
|
|||
assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", appErr.Id)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLoginByIntune_InterfaceNotAvailable tests that LoginByIntune returns proper error when enterprise not compiled
|
||||
func TestLoginByIntune_InterfaceNotAvailable(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Intune interface should be nil in non-enterprise setup
|
||||
require.Nil(t, th.App.Intune())
|
||||
|
||||
// Attempt login
|
||||
user, appErr := th.App.LoginByIntune(th.Context, "fake-token")
|
||||
|
||||
// Should return error
|
||||
require.Nil(t, user)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.user.login_by_intune.not_available.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusNotImplemented, appErr.StatusCode)
|
||||
}
|
||||
|
||||
// TestLoginByIntune_NotConfigured tests that LoginByIntune returns proper error when Intune not configured
|
||||
func TestLoginByIntune_NotConfigured(t *testing.T) {
|
||||
th := SetupEnterprise(t).InitBasic(t)
|
||||
|
||||
// Create mock Intune interface
|
||||
mockIntune := &mocks.IntuneInterface{}
|
||||
mockIntune.On("IsConfigured").Return(false)
|
||||
|
||||
// Replace Intune interface with mock
|
||||
originalIntune := th.App.ch.Intune
|
||||
th.App.ch.Intune = mockIntune
|
||||
defer func() {
|
||||
th.App.ch.Intune = originalIntune
|
||||
}()
|
||||
|
||||
// Attempt login
|
||||
user, appErr := th.App.LoginByIntune(th.Context, "fake-token")
|
||||
|
||||
// Should return error
|
||||
require.Nil(t, user)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.user.login_by_intune.not_configured.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
||||
|
||||
mockIntune.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// TestLoginByIntune_Success_Office365 tests successful login with Office365 auth service
|
||||
func TestLoginByIntune_Success_Office365(t *testing.T) {
|
||||
th := SetupEnterprise(t).InitBasic(t)
|
||||
|
||||
// Create test user with Office365 auth
|
||||
testUser, appErr := th.App.CreateUser(th.Context, &model.User{
|
||||
Email: "office365user@example.com",
|
||||
Username: "office365user",
|
||||
AuthService: model.ServiceOffice365,
|
||||
AuthData: model.NewPointer("test-oid-123"),
|
||||
EmailVerified: true,
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Create mock Intune interface
|
||||
mockIntune := &mocks.IntuneInterface{}
|
||||
mockIntune.On("IsConfigured").Return(true)
|
||||
mockIntune.On("Login", mock.Anything, "valid-token").Return(testUser, nil)
|
||||
|
||||
// Replace Intune interface with mock
|
||||
originalIntune := th.App.ch.Intune
|
||||
th.App.ch.Intune = mockIntune
|
||||
defer func() {
|
||||
th.App.ch.Intune = originalIntune
|
||||
}()
|
||||
|
||||
// Attempt login
|
||||
user, appErr := th.App.LoginByIntune(th.Context, "valid-token")
|
||||
|
||||
// Should succeed
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, user)
|
||||
assert.Equal(t, testUser.Id, user.Id)
|
||||
assert.Equal(t, model.ServiceOffice365, user.AuthService)
|
||||
|
||||
mockIntune.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// TestLoginByIntune_Success_SAML tests successful login with SAML auth service
|
||||
func TestLoginByIntune_Success_SAML(t *testing.T) {
|
||||
th := SetupEnterprise(t).InitBasic(t)
|
||||
|
||||
// Create test user with SAML auth
|
||||
testUser, appErr := th.App.CreateUser(th.Context, &model.User{
|
||||
Email: "samluser@example.com",
|
||||
Username: "samluser",
|
||||
AuthService: model.UserAuthServiceSaml,
|
||||
AuthData: model.NewPointer("test@example.com"),
|
||||
EmailVerified: true,
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Create mock Intune interface
|
||||
mockIntune := &mocks.IntuneInterface{}
|
||||
mockIntune.On("IsConfigured").Return(true)
|
||||
mockIntune.On("Login", mock.Anything, "valid-token").Return(testUser, nil)
|
||||
|
||||
// Replace Intune interface with mock
|
||||
originalIntune := th.App.ch.Intune
|
||||
th.App.ch.Intune = mockIntune
|
||||
defer func() {
|
||||
th.App.ch.Intune = originalIntune
|
||||
}()
|
||||
|
||||
// Attempt login
|
||||
user, appErr := th.App.LoginByIntune(th.Context, "valid-token")
|
||||
|
||||
// Should succeed
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, user)
|
||||
assert.Equal(t, testUser.Id, user.Id)
|
||||
assert.Equal(t, model.UserAuthServiceSaml, user.AuthService)
|
||||
|
||||
mockIntune.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// TestLoginByIntune_BotAccountBlocked tests that bot accounts cannot login via Intune
|
||||
func TestLoginByIntune_BotAccountBlocked(t *testing.T) {
|
||||
th := SetupEnterprise(t).InitBasic(t)
|
||||
|
||||
// Create bot account
|
||||
bot := th.CreateBot(t)
|
||||
botUser, appErr := th.App.GetUser(bot.UserId)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Create mock Intune interface that returns bot user
|
||||
mockIntune := &mocks.IntuneInterface{}
|
||||
mockIntune.On("IsConfigured").Return(true)
|
||||
mockIntune.On("Login", mock.Anything, "bot-token").Return(botUser, nil)
|
||||
|
||||
// Replace Intune interface with mock
|
||||
originalIntune := th.App.ch.Intune
|
||||
th.App.ch.Intune = mockIntune
|
||||
defer func() {
|
||||
th.App.ch.Intune = originalIntune
|
||||
}()
|
||||
|
||||
// Attempt login
|
||||
user, appErr := th.App.LoginByIntune(th.Context, "bot-token")
|
||||
|
||||
// Should be blocked
|
||||
require.Nil(t, user)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.user.login_by_intune.bot_login_forbidden.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusForbidden, appErr.StatusCode)
|
||||
|
||||
mockIntune.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// TestLoginByIntune_AccountLocked tests that deleted/locked accounts cannot login
|
||||
func TestLoginByIntune_AccountLocked(t *testing.T) {
|
||||
th := SetupEnterprise(t).InitBasic(t)
|
||||
|
||||
// Create user and then soft delete it
|
||||
deletedUser, appErr := th.App.CreateUser(th.Context, &model.User{
|
||||
Email: "deleteduser@example.com",
|
||||
Username: "deleteduser",
|
||||
AuthService: model.ServiceOffice365,
|
||||
AuthData: model.NewPointer("deleted-oid-123"),
|
||||
EmailVerified: true,
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Soft delete the user (deactivate)
|
||||
_, appErr = th.App.UpdateActive(th.Context, deletedUser, false)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Reload user to get updated DeleteAt
|
||||
deletedUser, appErr = th.App.GetUser(deletedUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Create mock Intune interface that returns deleted user
|
||||
mockIntune := &mocks.IntuneInterface{}
|
||||
mockIntune.On("IsConfigured").Return(true)
|
||||
mockIntune.On("Login", mock.Anything, "deleted-token").Return(deletedUser, nil)
|
||||
|
||||
// Replace Intune interface with mock
|
||||
originalIntune := th.App.ch.Intune
|
||||
th.App.ch.Intune = mockIntune
|
||||
defer func() {
|
||||
th.App.ch.Intune = originalIntune
|
||||
}()
|
||||
|
||||
// Attempt login
|
||||
user, appErr := th.App.LoginByIntune(th.Context, "deleted-token")
|
||||
|
||||
// Should be blocked
|
||||
require.Nil(t, user)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.user.login_by_intune.account_locked.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusConflict, appErr.StatusCode)
|
||||
|
||||
mockIntune.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// TestLoginByIntune_TokenValidationFailure tests that invalid tokens are rejected
|
||||
func TestLoginByIntune_TokenValidationFailure(t *testing.T) {
|
||||
th := SetupEnterprise(t).InitBasic(t)
|
||||
|
||||
// Create mock Intune interface that returns validation error
|
||||
mockIntune := &mocks.IntuneInterface{}
|
||||
mockIntune.On("IsConfigured").Return(true)
|
||||
mockIntune.On("Login", mock.Anything, "invalid-token").Return(nil, model.NewAppError(
|
||||
"IntuneInterface.Login",
|
||||
"ent.intune.validate_token.invalid_token.app_error",
|
||||
nil,
|
||||
"token validation failed",
|
||||
http.StatusBadRequest,
|
||||
))
|
||||
|
||||
// Replace Intune interface with mock
|
||||
originalIntune := th.App.ch.Intune
|
||||
th.App.ch.Intune = mockIntune
|
||||
defer func() {
|
||||
th.App.ch.Intune = originalIntune
|
||||
}()
|
||||
|
||||
// Attempt login
|
||||
user, appErr := th.App.LoginByIntune(th.Context, "invalid-token")
|
||||
|
||||
// Should return validation error
|
||||
require.Nil(t, user)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "ent.intune.validate_token.invalid_token.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
||||
|
||||
mockIntune.AssertExpectations(t)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ func (w *Web) InitOAuth() {
|
|||
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/mobile_login", w.APIHandler(mobileLoginWithOAuth)).Methods(http.MethodGet)
|
||||
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/signup", w.APIHandler(signupWithOAuth)).Methods(http.MethodGet)
|
||||
|
||||
// Intune MAM authentication endpoint
|
||||
w.MainRouter.Handle("/oauth/intune", w.APIHandler(loginByIntune)).Methods(http.MethodPost)
|
||||
|
||||
// Old endpoints for backwards compatibility, needed to not break SSO for any old setups
|
||||
w.MainRouter.Handle("/api/v3/oauth/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods(http.MethodGet)
|
||||
w.MainRouter.Handle("/signup/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods(http.MethodGet)
|
||||
|
|
@ -625,3 +628,52 @@ func getAuthorizationServerMetadata(c *Context, w http.ResponseWriter, r *http.R
|
|||
c.Logger.Warn("Error writing authorization server metadata response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
// loginByIntune handles authentication using Microsoft Intune MAM with Entra ID tokens
|
||||
func loginByIntune(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var req model.IntuneLoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
c.SetInvalidParamWithErr("request_body", err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AccessToken == "" {
|
||||
c.SetInvalidParam("access_token")
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("login_by_intune", model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
// Authenticate user via Intune MAM
|
||||
user, err := c.App.LoginByIntune(c.AppContext, req.AccessToken)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddMeta("obtained_user_id", user.Id)
|
||||
c.LogAuditWithUserId(user.Id, "obtained user")
|
||||
|
||||
isMobile := req.DeviceId != ""
|
||||
session, err := c.App.DoLogin(c.AppContext, w, r, user, req.DeviceId, isMobile, true, false)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
c.AppContext = c.AppContext.WithSession(session)
|
||||
|
||||
c.App.AttachSessionCookies(c.AppContext, w, r)
|
||||
|
||||
auditRec.Success()
|
||||
c.LogAuditWithUserId(user.Id, "success")
|
||||
|
||||
user.Sanitize(map[string]bool{})
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(user); err != nil {
|
||||
c.Logger.Warn("Failed to encode user response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,8 @@ func IsOAuthAPICall(a *app.App, r *http.Request) bool {
|
|||
|
||||
if r.URL.Path == path.Join(subpath, "oauth", "apps", "authorized") ||
|
||||
r.URL.Path == path.Join(subpath, "oauth", "deauthorize") ||
|
||||
r.URL.Path == path.Join(subpath, "oauth", "access_token") {
|
||||
r.URL.Path == path.Join(subpath, "oauth", "access_token") ||
|
||||
r.URL.Path == path.Join(subpath, "oauth", "intune") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -417,6 +418,23 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
|
|||
props["MobilePreventScreenCapture"] = strconv.FormatBool(*c.NativeAppSettings.MobilePreventScreenCapture)
|
||||
props["MobileJailbreakProtection"] = strconv.FormatBool(*c.NativeAppSettings.MobileJailbreakProtection)
|
||||
}
|
||||
|
||||
if model.MinimumEnterpriseAdvancedLicense(license) {
|
||||
// Check IntuneSettings configuration
|
||||
intuneEnabled := (c.IntuneSettings.Enable != nil && *c.IntuneSettings.Enable && c.IntuneSettings.IsValid() == nil)
|
||||
|
||||
props["IntuneMAMEnabled"] = strconv.FormatBool(intuneEnabled)
|
||||
|
||||
if intuneEnabled {
|
||||
// Use IntuneSettings for scope
|
||||
props["IntuneScope"] = fmt.Sprintf("api://%s/login.mattermost", *c.IntuneSettings.ClientId)
|
||||
|
||||
// Expose AuthService if set
|
||||
if c.IntuneSettings.AuthService != nil && *c.IntuneSettings.AuthService != "" {
|
||||
props["IntuneAuthService"] = *c.IntuneSettings.AuthService
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range c.FeatureFlags.ToMap() {
|
||||
|
|
|
|||
|
|
@ -423,6 +423,157 @@ func TestGetClientConfig(t *testing.T) {
|
|||
"BurnOnReadDurationMinutes": "10",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Intune MAM enabled with Enterprise Advanced license and Office365 AuthService",
|
||||
&model.Config{
|
||||
IntuneSettings: model.IntuneSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
TenantId: model.NewPointer("12345678-1234-1234-1234-123456789012"),
|
||||
ClientId: model.NewPointer("87654321-4321-4321-4321-210987654321"),
|
||||
AuthService: model.NewPointer(model.ServiceOffice365),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuEnterpriseAdvanced,
|
||||
},
|
||||
map[string]string{
|
||||
"IntuneMAMEnabled": "true",
|
||||
"IntuneScope": "api://87654321-4321-4321-4321-210987654321/login.mattermost",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Intune MAM disabled when not enabled",
|
||||
&model.Config{
|
||||
IntuneSettings: model.IntuneSettings{
|
||||
Enable: model.NewPointer(false),
|
||||
TenantId: model.NewPointer("12345678-1234-1234-1234-123456789012"),
|
||||
ClientId: model.NewPointer("87654321-4321-4321-4321-210987654321"),
|
||||
AuthService: model.NewPointer(model.ServiceOffice365),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuEnterpriseAdvanced,
|
||||
},
|
||||
map[string]string{
|
||||
"IntuneMAMEnabled": "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Intune MAM disabled when TenantId is missing",
|
||||
&model.Config{
|
||||
IntuneSettings: model.IntuneSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
TenantId: model.NewPointer(""),
|
||||
ClientId: model.NewPointer("87654321-4321-4321-4321-210987654321"),
|
||||
AuthService: model.NewPointer(model.ServiceOffice365),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuEnterpriseAdvanced,
|
||||
},
|
||||
map[string]string{
|
||||
"IntuneMAMEnabled": "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Intune MAM disabled when ClientId is missing",
|
||||
&model.Config{
|
||||
IntuneSettings: model.IntuneSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
TenantId: model.NewPointer("12345678-1234-1234-1234-123456789012"),
|
||||
ClientId: model.NewPointer(""),
|
||||
AuthService: model.NewPointer(model.ServiceOffice365),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuEnterpriseAdvanced,
|
||||
},
|
||||
map[string]string{
|
||||
"IntuneMAMEnabled": "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Intune MAM not exposed with lower license tier",
|
||||
&model.Config{
|
||||
IntuneSettings: model.IntuneSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
TenantId: model.NewPointer("12345678-1234-1234-1234-123456789012"),
|
||||
ClientId: model.NewPointer("87654321-4321-4321-4321-210987654321"),
|
||||
AuthService: model.NewPointer(model.ServiceOffice365),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuProfessional,
|
||||
},
|
||||
map[string]string{},
|
||||
},
|
||||
{
|
||||
"Intune MAM not exposed without license",
|
||||
&model.Config{
|
||||
IntuneSettings: model.IntuneSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
TenantId: model.NewPointer("12345678-1234-1234-1234-123456789012"),
|
||||
ClientId: model.NewPointer("87654321-4321-4321-4321-210987654321"),
|
||||
AuthService: model.NewPointer(model.ServiceOffice365),
|
||||
},
|
||||
},
|
||||
"",
|
||||
nil,
|
||||
map[string]string{},
|
||||
},
|
||||
{
|
||||
"Intune MAM enabled with Enterprise Advanced license and SAML AuthService",
|
||||
&model.Config{
|
||||
IntuneSettings: model.IntuneSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
TenantId: model.NewPointer("12345678-1234-1234-1234-123456789012"),
|
||||
ClientId: model.NewPointer("87654321-4321-4321-4321-210987654321"),
|
||||
AuthService: model.NewPointer(model.UserAuthServiceSaml),
|
||||
},
|
||||
SamlSettings: model.SamlSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuEnterpriseAdvanced,
|
||||
},
|
||||
map[string]string{
|
||||
"IntuneMAMEnabled": "true",
|
||||
"IntuneScope": "api://87654321-4321-4321-4321-210987654321/login.mattermost",
|
||||
"IntuneAuthService": "saml",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Intune MAM disabled when AuthService is missing",
|
||||
&model.Config{
|
||||
IntuneSettings: model.IntuneSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
TenantId: model.NewPointer("12345678-1234-1234-1234-123456789012"),
|
||||
ClientId: model.NewPointer("87654321-4321-4321-4321-210987654321"),
|
||||
AuthService: model.NewPointer(""),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuEnterpriseAdvanced,
|
||||
},
|
||||
map[string]string{
|
||||
"IntuneMAMEnabled": "false",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
|
|
|
|||
34
server/einterfaces/intune.go
Normal file
34
server/einterfaces/intune.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package einterfaces
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
)
|
||||
|
||||
// IntuneInterface provides methods for Microsoft Intune MAM authentication.
|
||||
// This allows mobile users to authenticate via Microsoft Entra ID (Azure AD) MSAL tokens
|
||||
// and map to existing users who login via Office 365 or SAML on other clients.
|
||||
type IntuneInterface interface {
|
||||
// IsConfigured checks if Intune MAM is properly configured and enabled.
|
||||
// Returns true if IntuneSettings.Enable is true and all required configuration is present.
|
||||
IsConfigured() bool
|
||||
|
||||
// Login authenticates a user using a Microsoft Entra ID access_token from MSAL.
|
||||
// The token is validated against Microsoft's JWKS endpoint with proper key rollover support.
|
||||
// The access_token's audience claim is validated against the tenant-specific IntuneScope
|
||||
// to ensure proper tenant isolation.
|
||||
// The user is then matched to an existing user based on the configured AuthService
|
||||
// (either 'office365' or 'saml'), or a new user is created if allowed.
|
||||
//
|
||||
// Parameters:
|
||||
// - rctx: Request context for logging and tracing
|
||||
// - accessToken: The access_token from MSAL authentication
|
||||
//
|
||||
// Returns:
|
||||
// - user: The authenticated user (matched or newly created)
|
||||
// - appError: Error if authentication fails
|
||||
Login(rctx request.CTX, accessToken string) (*model.User, *model.AppError)
|
||||
}
|
||||
80
server/einterfaces/mocks/IntuneInterface.go
Normal file
80
server/einterfaces/mocks/IntuneInterface.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make einterfaces-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
model "github.com/mattermost/mattermost/server/public/model"
|
||||
request "github.com/mattermost/mattermost/server/public/shared/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// IntuneInterface is an autogenerated mock type for the IntuneInterface type
|
||||
type IntuneInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// IsConfigured provides a mock function with no fields
|
||||
func (_m *IntuneInterface) IsConfigured() bool {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for IsConfigured")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Login provides a mock function with given fields: rctx, accessToken
|
||||
func (_m *IntuneInterface) Login(rctx request.CTX, accessToken string) (*model.User, *model.AppError) {
|
||||
ret := _m.Called(rctx, accessToken)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Login")
|
||||
}
|
||||
|
||||
var r0 *model.User
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(request.CTX, string) (*model.User, *model.AppError)); ok {
|
||||
return rf(rctx, accessToken)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(request.CTX, string) *model.User); ok {
|
||||
r0 = rf(rctx, accessToken)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.User)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(request.CTX, string) *model.AppError); ok {
|
||||
r1 = rf(rctx, accessToken)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewIntuneInterface creates a new instance of IntuneInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewIntuneInterface(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *IntuneInterface {
|
||||
mock := &IntuneInterface{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
|
@ -46,4 +46,6 @@ import (
|
|||
_ "github.com/mattermost/enterprise/message_export/csv_export"
|
||||
// Needed to ensure the init() method in the EE gets run
|
||||
_ "github.com/mattermost/enterprise/message_export/global_relay_export"
|
||||
// Needed to ensure the init() method in the EE gets run
|
||||
_ "github.com/mattermost/enterprise/intune"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4410,6 +4410,22 @@
|
|||
"id": "api.user.login_by_cws.invalid_token.app_error",
|
||||
"translation": "CWS token is not valid"
|
||||
},
|
||||
{
|
||||
"id": "api.user.login_by_intune.account_locked.app_error",
|
||||
"translation": "Your account has been deactivated. Please contact your system administrator."
|
||||
},
|
||||
{
|
||||
"id": "api.user.login_by_intune.bot_login_forbidden.app_error",
|
||||
"translation": "Bot accounts cannot sign in using Microsoft authentication."
|
||||
},
|
||||
{
|
||||
"id": "api.user.login_by_intune.not_available.app_error",
|
||||
"translation": "Microsoft Intune authentication is not available."
|
||||
},
|
||||
{
|
||||
"id": "api.user.login_by_intune.not_configured.app_error",
|
||||
"translation": "Microsoft Intune authentication is not configured."
|
||||
},
|
||||
{
|
||||
"id": "api.user.login_by_oauth.bot_login_forbidden.app_error",
|
||||
"translation": "Bot login is forbidden."
|
||||
|
|
@ -8584,6 +8600,38 @@
|
|||
"id": "ent.id_loaded.license_disable.app_error",
|
||||
"translation": "Your license does not support ID Loaded Push Notifications."
|
||||
},
|
||||
{
|
||||
"id": "ent.intune.login.account_not_found.app_error",
|
||||
"translation": "Your account isn't fully set up yet. Please sign in to Mattermost via the web or desktop app first."
|
||||
},
|
||||
{
|
||||
"id": "ent.intune.login.extract_auth_data.app_error",
|
||||
"translation": "Authentication failed. Please try again or contact your system administrator."
|
||||
},
|
||||
{
|
||||
"id": "ent.intune.login.not_configured.app_error",
|
||||
"translation": "Microsoft Intune authentication is not configured."
|
||||
},
|
||||
{
|
||||
"id": "ent.intune.validate_token.invalid_tenant_id.app_error",
|
||||
"translation": "Authentication failed. Invalid configuration."
|
||||
},
|
||||
{
|
||||
"id": "ent.intune.validate_token.invalid_token.app_error",
|
||||
"translation": "Authentication failed. Please try again."
|
||||
},
|
||||
{
|
||||
"id": "ent.intune.validate_token.jwks_init.app_error",
|
||||
"translation": "Authentication service initialization failed. Please contact your system administrator."
|
||||
},
|
||||
{
|
||||
"id": "ent.intune.validate_token.missing_claims.app_error",
|
||||
"translation": "Authentication failed. Required user information is missing."
|
||||
},
|
||||
{
|
||||
"id": "ent.intune.validate_token.token_expired.app_error",
|
||||
"translation": "Authentication session has expired. Please try signing in again."
|
||||
},
|
||||
{
|
||||
"id": "ent.jobs.start_synchronize_job.timeout",
|
||||
"translation": "Reached AD/LDAP synchronization job timeout."
|
||||
|
|
@ -9812,6 +9860,38 @@
|
|||
"id": "model.config.is_valid.import.retention_days_too_low.app_error",
|
||||
"translation": "Invalid value for RetentionDays. Value is too low."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.intune_auth_service.app_error",
|
||||
"translation": "Intune MAM Auth Service is required when Intune is enabled."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.intune_auth_service_invalid.app_error",
|
||||
"translation": "Intune MAM Auth Service must be either 'office365' or 'saml'."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.intune_client_id.app_error",
|
||||
"translation": "Intune MAM Client ID is required when Intune is enabled."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.intune_client_id_format.app_error",
|
||||
"translation": "Intune MAM Client ID must be a valid UUID."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.intune_requires_office365.app_error",
|
||||
"translation": "Intune MAM requires OpenID Connect (Office 365) to be enabled when Auth Service is set to 'office365'."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.intune_requires_saml.app_error",
|
||||
"translation": "Intune MAM requires SAML to be enabled when Auth Service is set to 'saml'."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.intune_tenant_id.app_error",
|
||||
"translation": "Intune MAM Tenant ID is required when Intune is enabled."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.intune_tenant_id_format.app_error",
|
||||
"translation": "Intune MAM Tenant ID must be a valid UUID."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.invalid_redis_db.app_error",
|
||||
"translation": "Redis DB must have a value greater or equal to zero."
|
||||
|
|
|
|||
|
|
@ -1373,6 +1373,69 @@ func (s *Office365Settings) SSOSettings() *SSOSettings {
|
|||
return &ssoSettings
|
||||
}
|
||||
|
||||
type IntuneSettings struct {
|
||||
Enable *bool `access:"mobile_intune"`
|
||||
TenantId *string `access:"mobile_intune"` // telemetry: none
|
||||
ClientId *string `access:"mobile_intune"` // telemetry: none
|
||||
AuthService *string `access:"mobile_intune"` // "office365" or "saml"
|
||||
}
|
||||
|
||||
func (s *IntuneSettings) SetDefaults() {
|
||||
if s.Enable == nil {
|
||||
s.Enable = NewPointer(false)
|
||||
}
|
||||
|
||||
if s.TenantId == nil {
|
||||
s.TenantId = NewPointer("")
|
||||
}
|
||||
|
||||
if s.ClientId == nil {
|
||||
s.ClientId = NewPointer("")
|
||||
}
|
||||
|
||||
// AuthService has no default - must be explicitly set
|
||||
if s.AuthService == nil {
|
||||
s.AuthService = NewPointer("")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntuneSettings) IsValid() *AppError {
|
||||
if s.Enable == nil || !*s.Enable {
|
||||
return nil // Disabled, no validation needed
|
||||
}
|
||||
|
||||
// Must have TenantId
|
||||
if s.TenantId == nil || *s.TenantId == "" {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.intune_tenant_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Validate TenantId format (UUID)
|
||||
uuidRegex := regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
||||
if !uuidRegex.MatchString(*s.TenantId) {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.intune_tenant_id_format.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Must have ClientId
|
||||
if s.ClientId == nil || *s.ClientId == "" {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.intune_client_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Validate ClientId format (UUID)
|
||||
if !uuidRegex.MatchString(*s.ClientId) {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.intune_client_id_format.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Must have valid AuthService
|
||||
if s.AuthService == nil || *s.AuthService == "" {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.intune_auth_service.app_error", nil, "AuthService is required", http.StatusBadRequest)
|
||||
}
|
||||
if *s.AuthService != UserAuthServiceSaml && *s.AuthService != ServiceOffice365 {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.intune_auth_service_invalid.app_error", nil, "AuthService must be 'office365' or 'saml'", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReplicaLagSettings struct {
|
||||
DataSource *string `access:"environment,write_restrictable,cloud_restrictable"` // telemetry: none
|
||||
QueryAbsoluteLag *string `access:"environment,write_restrictable,cloud_restrictable"` // telemetry: none
|
||||
|
|
@ -2990,6 +3053,7 @@ type NativeAppSettings struct {
|
|||
MobileJailbreakProtection *bool `access:"site_customization,write_restrictable"`
|
||||
MobileEnableSecureFilePreview *bool `access:"site_customization,write_restrictable"`
|
||||
MobileAllowPdfLinkNavigation *bool `access:"site_customization,write_restrictable"`
|
||||
EnableIntuneMAM *bool `access:"site_customization,write_restrictable"` // telemetry: none
|
||||
}
|
||||
|
||||
func (s *NativeAppSettings) SetDefaults() {
|
||||
|
|
@ -3032,6 +3096,10 @@ func (s *NativeAppSettings) SetDefaults() {
|
|||
if s.MobileAllowPdfLinkNavigation == nil {
|
||||
s.MobileAllowPdfLinkNavigation = NewPointer(false)
|
||||
}
|
||||
|
||||
if s.EnableIntuneMAM == nil {
|
||||
s.EnableIntuneMAM = NewPointer(false)
|
||||
}
|
||||
}
|
||||
|
||||
type ElasticsearchSettings struct {
|
||||
|
|
@ -3883,6 +3951,7 @@ type Config struct {
|
|||
LocalizationSettings LocalizationSettings
|
||||
SamlSettings SamlSettings
|
||||
NativeAppSettings NativeAppSettings
|
||||
IntuneSettings IntuneSettings
|
||||
CacheSettings CacheSettings
|
||||
ClusterSettings ClusterSettings
|
||||
MetricsSettings MetricsSettings
|
||||
|
|
@ -4003,6 +4072,7 @@ func (o *Config) SetDefaults() {
|
|||
o.AutoTranslationSettings.SetDefaults()
|
||||
o.ElasticsearchSettings.SetDefaults()
|
||||
o.NativeAppSettings.SetDefaults()
|
||||
o.IntuneSettings.SetDefaults()
|
||||
o.DataRetentionSettings.SetDefaults()
|
||||
o.RateLimitSettings.SetDefaults()
|
||||
o.LogSettings.SetDefaults()
|
||||
|
|
@ -4074,6 +4144,27 @@ func (o *Config) IsValid() *AppError {
|
|||
return appErr
|
||||
}
|
||||
|
||||
// Validate IntuneSettings
|
||||
if appErr := o.IntuneSettings.IsValid(); appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
// Cross-reference validation: IntuneSettings requires either Office365 or SAML to be enabled
|
||||
if o.IntuneSettings.Enable != nil && *o.IntuneSettings.Enable {
|
||||
if o.IntuneSettings.AuthService != nil && *o.IntuneSettings.AuthService != "" {
|
||||
switch *o.IntuneSettings.AuthService {
|
||||
case ServiceOffice365:
|
||||
if o.Office365Settings.Enable == nil || !*o.Office365Settings.Enable {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.intune_requires_office365.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
case UserAuthServiceSaml:
|
||||
if o.SamlSettings.Enable == nil || !*o.SamlSettings.Enable {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.intune_requires_saml.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *o.PasswordSettings.MinimumLength < PasswordMinimumLength || *o.PasswordSettings.MinimumLength > PasswordMaximumLength {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.password_length.app_error", map[string]any{"MinLength": PasswordMinimumLength, "MaxLength": PasswordMaximumLength}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,15 @@ const (
|
|||
OAuthActionMobile = "mobile"
|
||||
)
|
||||
|
||||
// IntuneLoginRequest represents a login request using an MSAL access_token from Azure AD/Entra
|
||||
// for Intune MAM authentication. The access_token is used instead of id_token to validate
|
||||
// the audience claim against the customer's tenant-specific IntuneScope, ensuring proper
|
||||
// tenant isolation.
|
||||
type IntuneLoginRequest struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
DeviceId string `json:"device_id"`
|
||||
}
|
||||
|
||||
type OAuthApp struct {
|
||||
Id string `json:"id"`
|
||||
CreatorId string `json:"creator_id"`
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ import {
|
|||
import AttributeBasedAccessControlFeatureDiscovery from './feature_discovery/features/attribute_based_access_control';
|
||||
import AutoTranslationFeatureDiscovery from './feature_discovery/features/auto_translation';
|
||||
import BurnOnReadSVG from './feature_discovery/features/images/burn_on_read_svg';
|
||||
import IntuneMAMSvg from './feature_discovery/features/images/intune_mam_svg';
|
||||
import UserAttributesFeatureDiscovery from './feature_discovery/features/user_attributes';
|
||||
import FeatureFlags, {messages as featureFlagsMessages} from './feature_flags';
|
||||
import GroupDetails from './group_settings/group_details';
|
||||
|
|
@ -2132,51 +2133,139 @@ const AdminDefinition: AdminDefinitionType = {
|
|||
schema: {
|
||||
id: 'MobileSecuritySettings',
|
||||
name: defineMessage({id: 'admin.mobileSecurity.title', defaultMessage: 'Mobile Security'}),
|
||||
settings: [
|
||||
sections: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'NativeAppSettings.MobileEnableBiometrics',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.biometricsTitle', defaultMessage: 'Enable Biometric Authentication:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.biometricsDescription', defaultMessage: 'Enforces biometric authentication (with PIN/passcode fallback) before accessing the app. Users will be prompted based on session activity and server switching rules.'}),
|
||||
key: 'MobileSecuritySettings.General',
|
||||
title: 'General Mobile Security',
|
||||
description: defineMessage({id: 'admin.mobileSecurity.sections.general.description', defaultMessage: 'Configure device security features for the mobile app.'}),
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'NativeAppSettings.MobileEnableBiometrics',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.biometricsTitle', defaultMessage: 'Enable Biometric Authentication:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.biometricsDescription', defaultMessage: 'Enforces biometric authentication (with PIN/passcode fallback) before accessing the app. Users will be prompted based on session activity and server switching rules.'}),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'NativeAppSettings.MobilePreventScreenCapture',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.screenCaptureTitle', defaultMessage: 'Prevent Screen Capture:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.screenCaptureDescription', defaultMessage: 'Blocks screenshots and screen recordings when using the mobile app. Screenshots will appear blank, and screen recordings will blur (iOS) or show a black screen (Android). Also applies when switching apps.'}),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'NativeAppSettings.MobileJailbreakProtection',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.jailbreakTitle', defaultMessage: 'Enable Jailbreak/Root Protection:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.jailbreakDescription', defaultMessage: 'Prevents access to the app on devices detected as jailbroken or rooted. If a device fails the security check, users will be denied access or prompted to switch to a compliant server.'}),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'NativeAppSettings.MobileEnableSecureFilePreview',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.secureFilePreviewTitle', defaultMessage: 'Enable Secure File Preview Mode:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.secureFilePreviewDescription', defaultMessage: "Prevents file downloads, previews, and sharing for most file types, even if {mobileAllowDownloads} is enabled. Allows in-app previews for PDFs, videos, and images only. Files are stored temporarily in the app's cache and cannot be exported or shared."}),
|
||||
help_text_values: {
|
||||
mobileAllowDownloads: (
|
||||
<a href='../site_config/file_sharing_downloads'>
|
||||
<b>
|
||||
<FormattedMessage
|
||||
id='admin.mobileSecurity.mobileAllowDownloads'
|
||||
defaultMessage='Site Configuration > File Sharing and Downloads > Allow File Downloads on Mobile'
|
||||
/>
|
||||
</b>
|
||||
</a>
|
||||
),
|
||||
},
|
||||
isHidden: it.not(it.minLicenseTier(LicenseSkus.EnterpriseAdvanced)),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'NativeAppSettings.MobileAllowPdfLinkNavigation',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.allowPdfLinkNavigationTitle', defaultMessage: 'Allow Link Navigation in Secure PDFs:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.allowPdfLinkNavigationDescription', defaultMessage: 'Enables tapping links inside PDFs when Secure File Preview Mode is active. Links will open in the device browser or supported app. Has no effect when Secure File Preview Mode is disabled.'}),
|
||||
isDisabled: it.stateIsFalse('NativeAppSettings.MobileEnableSecureFilePreview'),
|
||||
isHidden: it.not(it.minLicenseTier(LicenseSkus.EnterpriseAdvanced)),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'NativeAppSettings.MobilePreventScreenCapture',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.screenCaptureTitle', defaultMessage: 'Prevent Screen Capture:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.screenCaptureDescription', defaultMessage: 'Blocks screenshots and screen recordings when using the mobile app. Screenshots will appear blank, and screen recordings will blur (iOS) or show a black screen (Android). Also applies when switching apps.'}),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'NativeAppSettings.MobileJailbreakProtection',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.jailbreakTitle', defaultMessage: 'Enable Jailbreak/Root Protection:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.jailbreakDescription', defaultMessage: 'Prevents access to the app on devices detected as jailbroken or rooted. If a device fails the security check, users will be denied access or prompted to switch to a compliant server.'}),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'NativeAppSettings.MobileEnableSecureFilePreview',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.secureFilePreviewTitle', defaultMessage: 'Enable Secure File Preview Mode:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.secureFilePreviewDescription', defaultMessage: 'Prevents file downloads, previews, and sharing for most file types, even if {mobileAllowDownloads} is enabled. Allows in-app previews for PDFs, videos, and images only. Files are stored temporarily in the app’s cache and cannot be exported or shared.'}),
|
||||
help_text_values: {
|
||||
mobileAllowDownloads: (
|
||||
<a href='../site_config/file_sharing_downloads'>
|
||||
<b>
|
||||
<FormattedMessage
|
||||
id='admin.mobileSecurity.mobileAllowDownloads'
|
||||
defaultMessage='Site Configuration > File Sharing and Downloads > Allow File Downloads on Mobile'
|
||||
/>
|
||||
</b>
|
||||
</a>
|
||||
),
|
||||
key: 'MobileSecuritySettings.Intune',
|
||||
title: 'Microsoft Intune',
|
||||
description: defineMessage({id: 'admin.mobileSecurity.sections.intune.description', defaultMessage: 'Configure Microsoft Intune Mobile Application Management (MAM) for App Protection Policies.'}),
|
||||
license_sku: LicenseSkus.EnterpriseAdvanced,
|
||||
component: LicensedSectionContainer,
|
||||
componentProps: {
|
||||
requiredSku: LicenseSkus.EnterpriseAdvanced,
|
||||
featureDiscoveryConfig: {
|
||||
featureName: 'intune_mam',
|
||||
title: defineMessage({id: 'admin.intune_feature_discovery.title', defaultMessage: 'Protect mobile data with Microsoft Intune App Protection Policies (MAM) and Entra ID authentication'}),
|
||||
description: defineMessage({id: 'admin.intune_feature_discovery.description', defaultMessage: 'With Mattermost Enterprise Advanced, you can enable Microsoft Intune Mobile Application Management (MAM) to enforce App Protection Policies (APP) on Mattermost Mobile. Users sign in with Microsoft Entra ID (Azure AD), and Intune MAM applies data protection, selective wipe, and compliance policies on supported iOS devices.'}),
|
||||
learnMoreURL: 'https://docs.mattermost.com/deployment/intune-mam.html',
|
||||
svgImage: IntuneMAMSvg,
|
||||
},
|
||||
},
|
||||
isHidden: it.not(it.minLicenseTier(LicenseSkus.EnterpriseAdvanced)),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'NativeAppSettings.MobileAllowPdfLinkNavigation',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.allowPdfLinkNavigationTitle', defaultMessage: 'Allow Link Navigation in Secure PDFs:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.allowPdfLinkNavigationDescription', defaultMessage: 'Enables tapping links inside PDFs when Secure File Preview Mode is active. Links will open in the device browser or supported app. Has no effect when Secure File Preview Mode is disabled.'}),
|
||||
isDisabled: it.stateIsFalse('NativeAppSettings.MobileEnableSecureFilePreview'),
|
||||
isHidden: it.not(it.minLicenseTier(LicenseSkus.EnterpriseAdvanced)),
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'IntuneSettings.Enable',
|
||||
label: defineMessage({id: 'admin.intune.enableTitle', defaultMessage: 'Enable Microsoft Intune MAM:'}),
|
||||
help_text: defineMessage({id: 'admin.intune.enableDescription', defaultMessage: 'When enabled, Mattermost Mobile uses Microsoft Entra ID (Azure AD) for app authentication and policy enforcement. Users authenticate using MSAL tokens, and Intune MAM policies (App Protection Policies) are applied to protect corporate data.'}),
|
||||
},
|
||||
{
|
||||
type: 'dropdown',
|
||||
key: 'IntuneSettings.AuthService',
|
||||
label: defineMessage({id: 'admin.intune.authServiceTitle', defaultMessage: 'Auth Provider:'}),
|
||||
help_text: defineMessage({id: 'admin.intune.authServiceDescription', defaultMessage: 'Select how users authenticate into Mattermost.\n* **OpenID Connect** – Use when users sign in to Mattermost via Microsoft 365 / Entra ID using OIDC.\n* **SAML 2.0** – Use when users authenticate via a SAML provider that ultimately maps to Microsoft Entra ID.\nChoose the option that matches how your organization already authenticates users into Mattermost on other clients.'}),
|
||||
help_text_markdown: true,
|
||||
options: [
|
||||
{
|
||||
value: '',
|
||||
display_name: defineMessage({id: 'admin.intune.authServicePlaceholder', defaultMessage: 'Select an auth provider'}),
|
||||
},
|
||||
{
|
||||
value: 'office365',
|
||||
display_name: defineMessage({id: 'admin.intune.authServiceOffice365', defaultMessage: 'OpenID Connect (Office 365)'}),
|
||||
isHidden: it.configIsFalse('Office365Settings', 'Enable'),
|
||||
},
|
||||
{
|
||||
value: 'saml',
|
||||
display_name: defineMessage({id: 'admin.intune.authServiceSaml', defaultMessage: 'SAML 2.0'}),
|
||||
isHidden: it.configIsFalse('SamlSettings', 'Enable'),
|
||||
},
|
||||
],
|
||||
isDisabled: it.stateIsFalse('IntuneSettings.Enable'),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'IntuneSettings.TenantId',
|
||||
label: defineMessage({id: 'admin.intune.tenantIdTitle', defaultMessage: 'Tenant ID:'}),
|
||||
help_text: defineMessage({id: 'admin.intune.tenantIdDescription', defaultMessage: 'The Microsoft Entra ID (Azure AD) Tenant ID (also called the Directory ID).\nThis is the globally unique identifier that represents your organization in Microsoft Entra ID.\nMattermost uses this ID to validate tokens issued for Intune MAM.'}),
|
||||
placeholder: defineMessage({id: 'admin.intune.tenantIdPlaceholder', defaultMessage: 'E.g.: "12345678-1234-1234-1234-123456789012"'}),
|
||||
isDisabled: it.stateIsFalse('IntuneSettings.Enable'),
|
||||
default: (value, config, state) => {
|
||||
if (state['IntuneSettings.Enable'] && !state['IntuneSettings.TenantId']) {
|
||||
if (state['IntuneSettings.AuthService'] === 'office365' && config.Office365Settings?.DirectoryId) {
|
||||
return config.Office365Settings?.DirectoryId;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'IntuneSettings.ClientId',
|
||||
label: defineMessage({id: 'admin.intune.clientIdTitle', defaultMessage: 'Application (Client) ID:'}),
|
||||
help_text: defineMessage({id: 'admin.intune.clientIdDescription', defaultMessage: 'The Application (Client) ID of your Intune MAM–enabled app registration in Microsoft Entra ID.\nThis is the client identifier that the Mattermost Mobile app uses to request MSAL tokens for Intune MAM enrollment and policy evaluation.'}),
|
||||
placeholder: defineMessage({id: 'admin.intune.clientIdPlaceholder', defaultMessage: 'E.g.: "87654321-4321-4321-4321-210987654321"'}),
|
||||
isDisabled: it.stateIsFalse('IntuneSettings.Enable'),
|
||||
default: (value, config, state) => {
|
||||
if (state['IntuneSettings.Enable'] && !state['IntuneSettings.TenantId']) {
|
||||
if (state['IntuneSettings.AuthService'] === 'office365' && config.Office365Settings?.Id) {
|
||||
return config.Office365Settings?.Id;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,694 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type SvgProps = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const IntuneMAMSvg = (props: SvgProps) => (
|
||||
<svg
|
||||
width={props.width ? props.width.toString() : '284'}
|
||||
height={props.height ? props.height.toString() : '173'}
|
||||
viewBox='0 0 284 173'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M18.03 4.94443L18.03 25.5L214.53 25.5'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.32'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<ellipse
|
||||
cx='3'
|
||||
cy='3'
|
||||
rx='3'
|
||||
ry='3'
|
||||
transform='matrix(1 8.74228e-08 8.74228e-08 -1 15.03 6)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.48'
|
||||
/>
|
||||
<path
|
||||
d='M278.655 55.8479L278.655 19.4004L242.882 19.4004'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.32'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<circle
|
||||
cx='278.655'
|
||||
cy='59.2227'
|
||||
r='3.37477'
|
||||
transform='rotate(180 278.655 59.2227)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.48'
|
||||
/>
|
||||
<path
|
||||
d='M164.345 66.9186H230.528'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M174.202 75.3676H240.386'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M167.161 85.2247H233.345'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M179.835 102.123H210.814'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M158.711 58.6563H179.834'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M184.058 58.6563H215.038'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M219.262 58.6563H246.018'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M199.549 110.572H226.304'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M179.835 110.572H195.325'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M169.977 93.2667H200.957'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M205.181 93.2665H231.937'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M164.344 50.7246H199.549'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M205.181 50.7246H240.385'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M217.677 75.5825C219.143 75.5826 220.327 76.7619 220.327 78.2231V89.8784C220.327 99.8294 215.863 107.898 205.888 107.898H197.791C187.816 107.898 183.352 99.8294 183.352 89.8784V78.2231C183.352 76.7669 184.541 75.5826 186.002 75.5825H217.677ZM199.852 82.3871V91.5024L201.718 93.3676L199.852 95.2319V97.6948H204.955V87.5463L203.529 86.1205L204.955 84.6938V82.3871H199.852Z'
|
||||
fill='#1C58D9'
|
||||
/>
|
||||
<path
|
||||
d='M202.962 57.1323C210.145 57.1325 215.029 63.2033 215.029 70.6616V75.6411C215.029 77.5321 210.561 77.5346 210.561 75.6411V70.6616C210.561 65.5868 207.636 61.7212 203.619 61.7212H199.253C195.486 61.7214 192.311 65.5869 192.311 70.6616V75.6411C192.311 77.5357 187.852 77.5344 187.852 75.6411V70.6616C187.852 63.2032 192.736 57.1323 199.92 57.1323H202.962Z'
|
||||
fill='#1C58D9'
|
||||
/>
|
||||
<path
|
||||
d='M99.1957 12H200.804L207 151.5H93L99.1957 12Z'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.16'
|
||||
/>
|
||||
<rect
|
||||
width='138'
|
||||
height='7.5'
|
||||
transform='matrix(1 0 0 -1 81 159)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.32'
|
||||
/>
|
||||
<rect
|
||||
x='51'
|
||||
y='3'
|
||||
width='196.5'
|
||||
height='129'
|
||||
rx='6'
|
||||
fill='white'
|
||||
stroke='#3F4350'
|
||||
strokeWidth='6'
|
||||
/>
|
||||
<path
|
||||
d='M40.53 159H108.03V61.53C108.03 57.3713 104.659 54 100.5 54H48.06C43.9013 54 40.53 57.3713 40.53 61.53V159Z'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.16'
|
||||
/>
|
||||
<rect
|
||||
width='48'
|
||||
height='55.5'
|
||||
rx='7.53'
|
||||
transform='matrix(1 0 0 -1 219.03 135)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.16'
|
||||
/>
|
||||
<rect
|
||||
x='227.758'
|
||||
y='76.7439'
|
||||
width='49.9482'
|
||||
height='94.0884'
|
||||
rx='8.71189'
|
||||
fill='white'
|
||||
stroke='#3F4350'
|
||||
strokeWidth='3.48476'
|
||||
/>
|
||||
<path
|
||||
d='M229.5 84.875C229.5 81.3466 232.36 78.4862 235.889 78.4862H269.575C273.103 78.4862 275.963 81.3466 275.963 84.875V86.6173H229.5V84.875Z'
|
||||
fill='#1C58D9'
|
||||
fillOpacity='0.16'
|
||||
/>
|
||||
<path
|
||||
d='M237 117H268.5'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M238.5 121.5H266.927'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M237 126L270 126'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M237 144L270 144'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M238.5 135L267.005 135'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M234 112.5H241.5'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M244.5 112.5H257.806'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M245.5 148.5H258.806'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M261 112.5H272.492'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M251.54 139.5H263.032'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M241.647 139.5H248.3'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M238.5 130.5H251.806'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M255 130.5H266.492'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M237 108H250.5'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M253.5 108H268.5'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M262.27 121.46C263.153 121.46 263.866 122.17 263.867 123.05V130.071C263.867 136.065 261.177 140.924 255.169 140.924H250.292C244.285 140.924 241.596 136.064 241.596 130.071V123.05C241.596 122.173 242.312 121.461 243.192 121.46H262.27ZM251.535 125.559V131.049L252.658 132.172L251.535 133.295V134.779H254.608V128.667L253.749 127.808L254.608 126.949V125.559H251.535Z'
|
||||
fill='#1C58D9'
|
||||
/>
|
||||
<path
|
||||
d='M253.407 110.348C257.734 110.348 260.675 114.004 260.675 118.496V121.495C260.675 122.634 257.984 122.636 257.984 121.495V118.496C257.984 115.44 256.222 113.111 253.803 113.111H251.174C248.905 113.111 246.992 115.44 246.992 118.496C246.992 118.496 246.992 120.354 246.992 121.495C246.992 122.637 244.307 122.636 244.307 121.495V118.496C244.307 114.004 247.249 110.348 251.575 110.348H253.407Z'
|
||||
fill='#1C58D9'
|
||||
/>
|
||||
<circle
|
||||
cx='150'
|
||||
cy='67.5'
|
||||
r='52.5'
|
||||
fill='#1C58D9'
|
||||
fillOpacity='0.04'
|
||||
/>
|
||||
<g clipPath='url(#clip0_3753_1225)'>
|
||||
<mask
|
||||
id='mask0_3753_1225'
|
||||
style={{maskType: 'luminance'}}
|
||||
maskUnits='userSpaceOnUse'
|
||||
x='120'
|
||||
y='37'
|
||||
width='60'
|
||||
height='61'
|
||||
>
|
||||
<path
|
||||
d='M120 37.5H180V97.5H120V37.5Z'
|
||||
fill='white'
|
||||
/>
|
||||
</mask>
|
||||
<g mask='url(#mask0_3753_1225)'>
|
||||
<path
|
||||
d='M120 46.5675C120 38.9044 130.02 34.7096 136.64 39.601L151.03 50.2275L163.174 43.3458C169.593 39.8457 179.878 42.3563 180 51.9239V87.1833C180 94.8464 170.274 98.8416 163.655 93.9497L149.002 83.0907L136.843 89.9742C130.364 93.5077 120.01 90.7969 120.01 81.4852C120.01 70.3088 120 74.6002 120 46.5685V46.5675Z'
|
||||
fill='url(#paint0_linear_3753_1225)'
|
||||
/>
|
||||
<path
|
||||
d='M120 46.5675C120 38.9044 130.02 34.7096 136.64 39.601L151.03 50.2275L163.174 43.3458C169.593 39.8457 179.878 42.3563 180 51.9239V87.1833C180 94.8464 170.274 98.8416 163.655 93.9497L149.002 83.0907L136.843 89.9742C130.364 93.5077 120.01 90.7969 120.01 81.4852C120.01 70.3088 120 74.6002 120 46.5685V46.5675Z'
|
||||
fill='url(#paint1_radial_3753_1225)'
|
||||
/>
|
||||
<path
|
||||
d='M120 46.5675C120 38.9044 130.02 34.7096 136.64 39.601L151.03 50.2275L163.174 43.3458C169.593 39.8457 179.878 42.3563 180 51.9239V87.1833C180 94.8464 170.274 98.8416 163.655 93.9497L149.002 83.0907L136.843 89.9742C130.364 93.5077 120.01 90.7969 120.01 81.4852C120.01 70.3088 120 74.6002 120 46.5685V46.5675Z'
|
||||
fill='url(#paint2_radial_3753_1225)'
|
||||
/>
|
||||
<path
|
||||
d='M136.64 39.601C130.02 34.7096 120 38.9044 120 46.5675V81.0516C120 80.2135 120.427 78.5442 120.427 78.5442C122.401 72.0905 130.794 69.6708 136.601 73.9238L163.654 93.9502C170.274 98.8416 180 94.8464 180 87.1833V51.9239C180 58.47 171.443 65.0414 163.36 59.3475L136.64 39.601Z'
|
||||
fill='url(#paint3_radial_3753_1225)'
|
||||
/>
|
||||
<path
|
||||
d='M136.64 39.601C130.02 34.7096 120 38.9044 120 46.5675V81.0516C120 80.2135 120.427 78.5442 120.427 78.5442C122.401 72.0905 130.794 69.6708 136.601 73.9238L163.654 93.9502C170.274 98.8416 180 94.8464 180 87.1833V51.9239C180 58.47 171.443 65.0414 163.36 59.3475L136.64 39.601Z'
|
||||
fill='url(#paint4_radial_3753_1225)'
|
||||
fillOpacity='0.25'
|
||||
/>
|
||||
<path
|
||||
d='M136.64 39.601C130.02 34.7096 120 38.9044 120 46.5675V81.0516C120 80.2135 120.427 78.5442 120.427 78.5442C122.401 72.0905 130.794 69.6708 136.601 73.9238L163.654 93.9502C170.274 98.8416 180 94.8464 180 87.1833V51.9239C180 58.47 171.443 65.0414 163.36 59.3475L136.64 39.601Z'
|
||||
fill='url(#paint5_radial_3753_1225)'
|
||||
fillOpacity='0.55'
|
||||
/>
|
||||
<path
|
||||
d='M136.64 59.1434C130.02 54.2515 120 58.4468 120 66.1099V46.5677C120 38.9037 130.02 34.7088 136.64 39.6002L163.36 59.3468C171.443 65.0412 180 58.4698 180 51.9237V71.9234C180 79.5865 169.98 83.7813 163.36 78.8899L136.64 59.1434Z'
|
||||
fill='url(#paint6_linear_3753_1225)'
|
||||
/>
|
||||
<path
|
||||
d='M136.64 59.1434C130.02 54.2515 120 58.4468 120 66.1099V46.5677C120 38.9037 130.02 34.7088 136.64 39.6002L163.36 59.3468C171.443 65.0412 180 58.4698 180 51.9237V71.9234C180 79.5865 169.98 83.7813 163.36 78.8899L136.64 59.1434Z'
|
||||
fill='url(#paint7_radial_3753_1225)'
|
||||
fillOpacity='0.36'
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<rect
|
||||
x='99.75'
|
||||
y='42.75'
|
||||
width='127.5'
|
||||
height='97.5'
|
||||
rx='5.63698'
|
||||
transform='rotate(90 99.75 42.75)'
|
||||
fill='white'
|
||||
stroke='#3F4350'
|
||||
strokeWidth='4.5'
|
||||
/>
|
||||
<path
|
||||
d='M4.5 46.4559C4.5 45.6518 5.15182 45 5.95588 45H96.0441C96.8482 45 97.5 45.6518 97.5 46.4559V54H4.5V46.4559Z'
|
||||
fill='#1C58D9'
|
||||
fillOpacity='0.16'
|
||||
/>
|
||||
<path
|
||||
d='M61.4471 101.888C62.3956 101.888 63.1618 102.651 63.162 103.596V111.138C63.162 117.577 60.2733 122.798 53.8192 122.798H48.58C42.1259 122.798 39.2372 117.577 39.2372 111.138V103.596C39.2374 102.654 40.0068 101.888 40.952 101.888H61.4471ZM49.9139 106.291V112.19L51.12 113.396L49.9139 114.602V116.197H53.2157V109.629L52.2928 108.707L53.2157 107.784V106.291H49.9139Z'
|
||||
fill='#1C58D9'
|
||||
/>
|
||||
<path
|
||||
d='M51.9254 89.9496C56.5733 89.9496 59.7339 93.8775 59.7339 98.7036V101.926C59.7334 103.149 56.8439 103.151 56.8433 101.926V98.7036C56.8433 95.4199 54.9506 92.9194 52.3511 92.9194H49.5259C47.0888 92.9194 45.0338 95.4199 45.0338 98.7036C45.0338 98.7036 45.0338 100.7 45.0338 101.926C45.0332 103.152 42.1496 103.151 42.149 101.926V98.7036C42.149 93.8775 45.3096 89.9496 49.9576 89.9496H51.9254Z'
|
||||
fill='#1C58D9'
|
||||
/>
|
||||
<path
|
||||
d='M23.9415 94.5H75.9138'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M26.4163 99H73.3185'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M23.9413 103.5L78.3885 103.5'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M23.9413 121.5L78.3885 121.5'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M26.4163 112.5L73.4468 112.5'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M18.9916 90H31.366'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M36.3157 90H58.27'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M38.2906 126H60.2448'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M63.5393 90H82.4998'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M47.9312 117H66.8917'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M31.6083 117H42.5854'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M26.4163 108H48.3705'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M53.6398 108H72.6003'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M23.9413 85.5H46.2152'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M51.165 85.5H75.9137'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id='paint0_linear_3753_1225'
|
||||
x1='145.622'
|
||||
y1='96.0938'
|
||||
x2='167.036'
|
||||
y2='50.2744'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop
|
||||
offset='0.02'
|
||||
stopColor='#1169DA'
|
||||
/>
|
||||
<stop
|
||||
offset='0.435'
|
||||
stopColor='#0151BD'
|
||||
/>
|
||||
<stop
|
||||
offset='0.614'
|
||||
stopColor='#014DB7'
|
||||
/>
|
||||
<stop
|
||||
offset='1'
|
||||
stopColor='#126AD9'
|
||||
/>
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id='paint1_radial_3753_1225'
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(173.696 29.3841) rotate(110.224) scale(43.8371 32.8375)'
|
||||
>
|
||||
<stop
|
||||
offset='0.423'
|
||||
stopColor='#004AFF'
|
||||
stopOpacity='0.1'
|
||||
/>
|
||||
<stop
|
||||
offset='0.729'
|
||||
stopColor='#014DB9'
|
||||
/>
|
||||
<stop
|
||||
offset='0.836'
|
||||
stopColor='#014DB9'
|
||||
stopOpacity='0.9'
|
||||
/>
|
||||
<stop
|
||||
offset='0.955'
|
||||
stopColor='#014DB9'
|
||||
stopOpacity='0'
|
||||
/>
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id='paint2_radial_3753_1225'
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(128.01 94.3032) rotate(-73.6432) scale(34.9002 42.5128)'
|
||||
>
|
||||
<stop
|
||||
offset='0.091'
|
||||
stopColor='#004AFF'
|
||||
stopOpacity='0.1'
|
||||
/>
|
||||
<stop
|
||||
offset='0.56'
|
||||
stopColor='#014DB9'
|
||||
/>
|
||||
<stop
|
||||
offset='0.749'
|
||||
stopColor='#014DB9'
|
||||
stopOpacity='0.99'
|
||||
/>
|
||||
<stop
|
||||
offset='1'
|
||||
stopColor='#014DB9'
|
||||
stopOpacity='0'
|
||||
/>
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id='paint3_radial_3753_1225'
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(185.814 37.5) rotate(134.068) scale(84.5934 65.1563)'
|
||||
>
|
||||
<stop
|
||||
offset='0.249'
|
||||
stopColor='#23C0FE'
|
||||
/>
|
||||
<stop
|
||||
offset='0.717'
|
||||
stopColor='#23C0FE'
|
||||
/>
|
||||
<stop
|
||||
offset='0.995'
|
||||
stopColor='#1C91FF'
|
||||
/>
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id='paint4_radial_3753_1225'
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(137.891 42.2564) rotate(45.9092) scale(60.5184 14.3969)'
|
||||
>
|
||||
<stop
|
||||
offset='0.165'
|
||||
stopColor='#096DD6'
|
||||
stopOpacity='0'
|
||||
/>
|
||||
<stop
|
||||
offset='0.484'
|
||||
stopColor='#096DD6'
|
||||
/>
|
||||
<stop
|
||||
offset='0.901'
|
||||
stopColor='#0876DE'
|
||||
stopOpacity='0.814'
|
||||
/>
|
||||
<stop
|
||||
offset='1'
|
||||
stopColor='#029AFF'
|
||||
stopOpacity='0'
|
||||
/>
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id='paint5_radial_3753_1225'
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(177.764 73.4433) rotate(91.61) scale(9.71986 8.75334)'
|
||||
>
|
||||
<stop stopColor='#0068B3'/>
|
||||
<stop
|
||||
offset='0.93'
|
||||
stopColor='#006CB8'
|
||||
stopOpacity='0'
|
||||
/>
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id='paint6_linear_3753_1225'
|
||||
x1='137.105'
|
||||
y1='51.3181'
|
||||
x2='147.207'
|
||||
y2='94.2443'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop stopColor='#92EEFE'/>
|
||||
<stop
|
||||
offset='0.565'
|
||||
stopColor='#35DDFF'
|
||||
/>
|
||||
<stop
|
||||
offset='1'
|
||||
stopColor='#08B1F9'
|
||||
/>
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id='paint7_radial_3753_1225'
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(130.064 37.4999) rotate(69.461) scale(14.9496 12.0748)'
|
||||
>
|
||||
<stop
|
||||
stopColor='#CCF9FF'
|
||||
stopOpacity='0.93'
|
||||
/>
|
||||
<stop
|
||||
offset='1'
|
||||
stopColor='#35DDFF'
|
||||
/>
|
||||
</radialGradient>
|
||||
<clipPath id='clip0_3753_1225'>
|
||||
<rect
|
||||
width='60'
|
||||
height='60'
|
||||
fill='white'
|
||||
transform='translate(120 37.5)'
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default IntuneMAMSvg;
|
||||
|
|
@ -10,207 +10,168 @@ type SvgProps = {
|
|||
|
||||
const MobileSecuritySVG = (props: SvgProps) => (
|
||||
<svg
|
||||
width={props.width ? props.width.toString() : '208'}
|
||||
width={props.width ? props.width.toString() : '198'}
|
||||
height={props.height ? props.height.toString() : '120'}
|
||||
viewBox='0 0 208 120'
|
||||
viewBox='0 0 198 120'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<rect
|
||||
x='10.7451'
|
||||
y='22'
|
||||
width='181'
|
||||
height='78'
|
||||
rx='5.625'
|
||||
fill='#1C58D9'
|
||||
fillOpacity='0.12'
|
||||
/>
|
||||
<path
|
||||
d='M12 28L17.5 33.5V72.5H182.5V101.5'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.24'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<circle
|
||||
cx='2.5'
|
||||
cy='2.5'
|
||||
r='2.5'
|
||||
transform='matrix(1 0 0 -1 180 105)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.48'
|
||||
/>
|
||||
<circle
|
||||
cx='2.5'
|
||||
cy='2.5'
|
||||
r='2.5'
|
||||
transform='matrix(1 0 0 -1 8 36)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.48'
|
||||
/>
|
||||
<ellipse
|
||||
cx='102.5'
|
||||
cy='60.5'
|
||||
rx='59.5'
|
||||
ry='59.5'
|
||||
fill='#1C58D9'
|
||||
fillOpacity='0.04'
|
||||
/>
|
||||
<path
|
||||
d='M50 41L50 49.7097L148.46 49.7097L148.46 59L163 59'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.24'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M136 100L136 85L121 85'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.24'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<ellipse
|
||||
cx='1.5'
|
||||
cy='1.5'
|
||||
rx='1.5'
|
||||
ry='1.5'
|
||||
transform='matrix(1 8.74228e-08 8.74228e-08 -1 48.5 42)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.48'
|
||||
/>
|
||||
<ellipse
|
||||
cx='136'
|
||||
cy='101.5'
|
||||
rx='1.5'
|
||||
ry='1.5'
|
||||
transform='rotate(180 136 101.5)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.48'
|
||||
/>
|
||||
<ellipse
|
||||
cx='1.5'
|
||||
cy='1.5'
|
||||
rx='1.5'
|
||||
ry='1.5'
|
||||
transform='matrix(1 8.74228e-08 8.74228e-08 -1 162 70)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.48'
|
||||
/>
|
||||
<path
|
||||
d='M102.238 107.436L102.482 107.572L102.725 107.437L107.738 104.652L107.738 104.652C134.208 89.9349 140.141 62.6051 142.498 32.2629L142.533 31.8248L142.102 31.7348L141.977 31.7086L141.927 31.698H141.875C129.258 31.698 116.365 27.1582 102.822 14.6329L102.444 14.2833L102.105 14.6715C96.4271 21.1871 91.4818 25.1561 85.8022 27.6605C80.114 30.1687 73.6481 31.2268 64.8879 31.85L64.8664 31.8515L64.8451 31.8549L62.9217 32.1596L62.4635 32.2323L62.5017 32.6946C64.9873 62.7953 70.8178 89.8754 97.0623 104.543L102.238 107.436Z'
|
||||
fill='white'
|
||||
stroke='#3F4350'
|
||||
/>
|
||||
<path
|
||||
d='M70.2469 81.6538C73.544 87.9479 77.7115 93.688 83.0142 98.7308M84.5619 100.154C85.6612 101.131 86.8066 102.08 88 103'
|
||||
stroke='#3F4350'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M102.485 99L98.0951 96.6096C76.045 84.6062 71.1078 62.4456 69 37.5833L70.6312 37.3316C85.5165 36.3001 92.8039 33.8204 102.485 23C114.04 33.4089 125.074 37.207 135.894 37.207L136 37.2287C134.001 62.2886 128.977 84.6549 106.736 96.6989L102.485 99Z'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.12'
|
||||
/>
|
||||
<path
|
||||
d='M77.5632 51.6124H121.686'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M84.1345 57.2451H128.257'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M79.4407 63.8165H123.563'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M87.8896 75.0818H108.543'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M73.8076 46.1042H87.8893'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M90.7056 46.1042H111.359'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M114.175 46.1042H132.012'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M101.033 80.7149H118.869'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M87.8899 80.7149H98.2164'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M81.3184 69.1778H101.971'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M104.788 69.1777H122.624'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M77.563 40.8164H101.032'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M104.788 40.8164H128.257'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M92.0013 57.3883H113.118C114.095 57.3883 114.885 58.1742 114.885 59.1484V66.9186C114.885 73.5528 111.909 78.9321 105.259 78.9321H99.8606C93.2109 78.9321 90.2349 73.5528 90.2349 66.9186V59.1484C90.2349 58.1775 91.0273 57.3883 92.0013 57.3883ZM102.172 61.9248H101.235V64.127V68.0017L102.478 69.2449L101.235 70.4881V72.1297H104.636V65.3642L103.686 64.4136L104.636 63.4629V61.9248H102.172Z'
|
||||
fill='#1C58D9'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M108.374 54.1074V57.4273C108.374 57.4476 108.375 57.4675 108.376 57.4871H111.35C111.352 57.4675 111.352 57.4476 111.352 57.4273V54.1074C111.352 49.1351 108.096 45.0882 103.308 45.0882H101.28C96.4909 45.0882 93.2349 49.1351 93.2349 54.1074V57.4273C93.2349 57.4476 93.2356 57.4675 93.2371 57.4871H96.205C96.2066 57.4675 96.2073 57.4476 96.2073 57.4273V54.1074C96.2073 50.7242 98.3244 48.1476 100.835 48.1476H103.746C106.424 48.1476 108.374 50.7242 108.374 54.1074Z'
|
||||
fill='#1C58D9'
|
||||
/>
|
||||
<path
|
||||
d='M109 16C109 16 118.172 25.6296 137 29'
|
||||
stroke='#3F4350'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<g clipPath='url(#clip0_3922_6263)'>
|
||||
<path
|
||||
d='M7 28L12.5 33.5V72.5H177.5V101.5'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.24'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<ellipse
|
||||
cx='97.5'
|
||||
cy='60.5'
|
||||
rx='59.5'
|
||||
ry='59.5'
|
||||
fill='#1C58D9'
|
||||
fillOpacity='0.08'
|
||||
/>
|
||||
<path
|
||||
d='M51 41L51 49.7097L143.46 49.7097L143.46 59L158 59'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.24'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M131 100L131 85L116 85'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.24'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M52.5 40.5C52.5 39.6716 51.8284 39 51 39C50.1716 39 49.5 39.6716 49.5 40.5C49.5 41.3284 50.1716 42 51 42C51.8284 42 52.5 41.3284 52.5 40.5Z'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.48'
|
||||
/>
|
||||
<ellipse
|
||||
cx='131'
|
||||
cy='101.5'
|
||||
rx='1.5'
|
||||
ry='1.5'
|
||||
transform='rotate(180 131 101.5)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.48'
|
||||
/>
|
||||
<ellipse
|
||||
cx='1.5'
|
||||
cy='1.5'
|
||||
rx='1.5'
|
||||
ry='1.5'
|
||||
transform='matrix(1 8.74228e-08 8.74228e-08 -1 157 70)'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.48'
|
||||
/>
|
||||
<path
|
||||
d='M97.8223 14.6328C111.365 27.158 124.258 31.6982 136.875 31.6982H136.927L136.978 31.709L137.103 31.7344L137.532 31.8252L137.498 32.2627C135.141 62.6049 129.208 89.9348 102.737 104.651L97.7256 107.438L97.4814 107.572L97.2383 107.437L92.0625 104.543C65.818 89.8755 59.9876 62.795 57.502 32.6943L57.4639 32.2324L57.9219 32.1592L59.8447 31.8545L59.8662 31.8516L59.8877 31.8496C68.6477 31.2264 75.1136 30.1683 80.8018 27.6602C86.4813 25.1558 91.4272 21.1874 97.1055 14.6719L97.4434 14.2832L97.8223 14.6328Z'
|
||||
fill='white'
|
||||
stroke='#3F4350'
|
||||
/>
|
||||
<path
|
||||
d='M97.485 99L93.0951 96.6096C71.045 84.6062 66.1078 62.4456 64 37.5833L65.6312 37.3316C80.5165 36.3001 87.8039 33.8204 97.485 23C109.04 33.4089 120.074 37.207 130.894 37.207L131 37.2287C129.001 62.2886 123.977 84.6549 101.736 96.6989L97.485 99Z'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.12'
|
||||
/>
|
||||
<path
|
||||
d='M72.5632 51.6124H116.686'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M79.1345 57.2451H123.257'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M74.4407 63.8165H118.563'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M82.8896 75.0818H103.543'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M68.8076 46.1042H82.8893'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M85.7056 46.1042H106.359'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M109.175 46.1042H127.012'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M96.0327 80.7149H113.869'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M82.8899 80.7149H93.2164'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M76.3184 69.1778H96.9714'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M99.7876 69.1777H117.624'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M72.563 40.8164H96.0324'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M99.7876 40.8164H123.257'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.12'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M87.0013 57.3883H108.118C109.095 57.3883 109.885 58.1742 109.885 59.1484V66.9186C109.885 73.5528 106.909 78.9321 100.259 78.9321H94.8606C88.2109 78.9321 85.2349 73.5528 85.2349 66.9186V59.1484C85.2349 58.1775 86.0273 57.3883 87.0013 57.3883ZM97.1724 61.9248H96.2348V64.127V68.0017L97.478 69.2449L96.2348 70.4881V72.1297H99.6365V65.3642L98.6858 64.4136L99.6365 63.4629V61.9248H97.1724Z'
|
||||
fill='#1C58D9'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M103.374 54.1074V57.4273C103.374 57.4476 103.375 57.4675 103.376 57.4871H106.35C106.352 57.4675 106.352 57.4476 106.352 57.4273V54.1074C106.352 49.1351 103.096 45.0882 98.3075 45.0882H96.2798C91.4909 45.0882 88.2349 49.1351 88.2349 54.1074V57.4273C88.2349 57.4476 88.2356 57.4675 88.2371 57.4871H91.205C91.2066 57.4675 91.2073 57.4476 91.2073 57.4273V54.1074C91.2073 50.7242 93.3244 48.1476 95.8353 48.1476H98.7461C101.424 48.1476 103.374 50.7242 103.374 54.1074Z'
|
||||
fill='#1C58D9'
|
||||
/>
|
||||
</g>
|
||||
<rect
|
||||
x='1.5'
|
||||
y='20.5'
|
||||
width='43'
|
||||
height='79'
|
||||
height='81'
|
||||
rx='7.5'
|
||||
fill='white'
|
||||
stroke='#3F4350'
|
||||
|
|
@ -226,7 +187,7 @@ const MobileSecuritySVG = (props: SvgProps) => (
|
|||
cy='64.831'
|
||||
r='5.83099'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.32'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
d='M9.69995 64.7411L12.0947 67.1786L16.2 63'
|
||||
|
|
@ -281,79 +242,79 @@ const MobileSecuritySVG = (props: SvgProps) => (
|
|||
/>
|
||||
<rect
|
||||
opacity='0.16'
|
||||
x='157'
|
||||
y='23'
|
||||
width='49'
|
||||
height='64'
|
||||
x='148'
|
||||
y='24.9268'
|
||||
width='47.8049'
|
||||
height='77.0732'
|
||||
rx='4'
|
||||
fill='#090A0B'
|
||||
/>
|
||||
<rect
|
||||
x='159.5'
|
||||
y='19.5'
|
||||
width='48'
|
||||
height='65'
|
||||
x='150.451'
|
||||
y='22.5'
|
||||
width='45.8049'
|
||||
height='78.0488'
|
||||
rx='3.5'
|
||||
fill='white'
|
||||
stroke='#3F4350'
|
||||
/>
|
||||
<path
|
||||
d='M185 38H201'
|
||||
d='M171 87.9032H191'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.56'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M184 65H200'
|
||||
d='M181 92.9032H191'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.56'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M184 71H200'
|
||||
d='M175.317 38.8117H190.927'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.56'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M165 47H202'
|
||||
d='M156.805 47.5922H190.902'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.56'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M165 53H202'
|
||||
d='M156.805 53.4459H190.902'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.56'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M185 32H194.635'
|
||||
d='M175.317 32.9581H184.717'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.56'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M196.932 32H199.946'
|
||||
d='M186.958 32.9581H189.899'
|
||||
stroke='#3F4350'
|
||||
strokeOpacity='0.56'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M171.467 26.918H173.457L173.709 28.5555C174.318 28.6939 174.888 28.9343 175.401 29.2577L176.74 28.2756L178.147 29.6827L177.153 31.0376C177.462 31.545 177.691 32.1069 177.822 32.7055L179.505 32.9645V34.9544L177.778 35.2201C177.634 35.7869 177.401 36.3184 177.094 36.7986L178.147 38.2337L176.74 39.6407L175.271 38.5634C174.801 38.8416 174.286 39.0514 173.74 39.179L173.46 41H171.47L171.188 39.1638C170.651 39.0316 170.145 38.8198 169.684 38.5418L168.186 39.6406L166.779 38.2335L167.867 36.7505C167.577 36.2856 167.355 35.774 167.215 35.2294L165.422 34.9535V32.9636L167.172 32.6944C167.299 32.1186 167.516 31.577 167.809 31.0854L166.78 29.6826L168.187 28.2755L169.556 29.2795C170.058 28.9575 170.616 28.7157 171.213 28.5718L171.467 26.918ZM175.147 33.8704C175.147 35.335 173.96 36.5222 172.495 36.5222C171.031 36.5222 169.844 35.335 169.844 33.8704C169.844 32.4059 171.031 31.2187 172.495 31.2187C173.96 31.2187 175.147 32.4059 175.147 33.8704Z'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
opacity='0.7'
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M172.405 61.9492H174.038L174.244 63.2907C174.745 63.4044 175.213 63.602 175.635 63.8679L176.73 63.0643L177.885 64.2188L177.071 65.3279C177.325 65.744 177.513 66.2048 177.619 66.6957L179 66.9081V68.5407L177.584 68.7585C177.465 69.2245 177.274 69.6613 177.022 70.0558L177.885 71.2329L176.73 72.3874L175.525 71.5032C175.14 71.7307 174.719 71.9023 174.272 72.0069L174.042 73.5039H172.409L172.177 71.9948C171.737 71.8867 171.324 71.7135 170.946 71.4861L169.715 72.3888L168.561 71.2344L169.453 70.0171C169.215 69.6355 169.033 69.2155 168.918 68.7683L167.445 68.5418V66.9092L168.881 66.6882C168.985 66.2163 169.163 65.7723 169.403 65.3692L168.56 64.2202L169.715 63.0657L170.835 63.887C171.247 63.6218 171.706 63.4226 172.197 63.3043L172.405 61.9492ZM175.426 67.6523C175.426 68.8539 174.452 69.828 173.25 69.828C172.048 69.828 171.074 68.8539 171.074 67.6523C171.074 66.4507 172.048 65.4766 173.25 65.4766C174.452 65.4766 175.426 66.4507 175.426 67.6523Z'
|
||||
d='M163.302 29.5986C163.895 29.7336 164.45 29.968 164.95 30.2832L166.259 29.3242L167.631 30.6973L166.661 32.0186C166.963 32.5137 167.185 33.0623 167.312 33.6465L168.956 33.8994V35.8408L167.27 36.0996C167.129 36.6524 166.901 37.1694 166.602 37.6377L167.631 39.04L166.258 40.4121L164.823 39.3604C164.365 39.6314 163.864 39.8356 163.332 39.96L163.058 41.7383H161.117L160.841 39.9453C160.317 39.8163 159.824 39.611 159.375 39.3398L157.913 40.4121L156.541 39.0391L157.601 37.5908C157.319 37.1376 157.103 36.6392 156.967 36.1084L155.217 35.8398V33.8984L156.926 33.6348C157.049 33.0737 157.26 32.5456 157.546 32.0664L156.542 30.6973L157.915 29.3242L159.252 30.3037C159.741 29.9902 160.284 29.7546 160.865 29.6143L161.114 28H163.055L163.302 29.5986ZM162.117 32.1963C160.688 32.1964 159.53 33.3545 159.53 34.7832C159.53 36.2117 160.688 37.37 162.117 37.3701C163.546 37.3701 164.704 36.2118 164.704 34.7832C164.704 33.3545 163.546 32.1963 162.117 32.1963Z'
|
||||
fill='#3F4350'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<defs>
|
||||
<clipPath id='clip0_3922_6263'>
|
||||
<rect
|
||||
width='136'
|
||||
height='119'
|
||||
fill='white'
|
||||
transform='translate(30 1)'
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ export class SchemaAdminSettings extends React.PureComponent<SchemaAdminSettings
|
|||
} else if (setting.multiple) {
|
||||
value = this.state[setting.key] ? this.state[setting.key].join(',') : '';
|
||||
} else {
|
||||
value = this.state[setting.key] ?? (setting.default || '');
|
||||
value = this.state[setting.key] ?? (typeof setting.default === 'function' ? setting.default(value, this.props.config, this.state) : setting.default || '');
|
||||
}
|
||||
|
||||
let footer = null;
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export type AdminDefinitionSettingInput = AdminDefinitionSettingBase & {
|
|||
setFromMetadataField?: string;
|
||||
dynamic_value?: (value: any, config: Partial<AdminConfig>, state: any) => string;
|
||||
max_length?: number;
|
||||
default?: string;
|
||||
default?: string | ((value: any, config: Partial<AdminConfig>, state: any) => string);
|
||||
}
|
||||
|
||||
type AdminDefinitionSettingGenerated = AdminDefinitionSettingBase & {
|
||||
|
|
|
|||
|
|
@ -1383,6 +1383,21 @@
|
|||
"admin.integrations.gif": "GIF",
|
||||
"admin.integrations.integrationManagement": "Integration Management",
|
||||
"admin.integrations.integrationManagement.title": "Integration Management",
|
||||
"admin.intune_feature_discovery.description": "With Mattermost Enterprise Advanced, you can enable Microsoft Intune Mobile Application Management (MAM) to enforce App Protection Policies (APP) on Mattermost Mobile. Users sign in with Microsoft Entra ID (Azure AD), and Intune MAM applies data protection, selective wipe, and compliance policies on supported iOS devices.",
|
||||
"admin.intune_feature_discovery.title": "Protect mobile data with Microsoft Intune App Protection Policies (MAM) and Entra ID authentication",
|
||||
"admin.intune.authServiceDescription": "Select how users authenticate into Mattermost.\n* **OpenID Connect** – Use when users sign in to Mattermost via Microsoft 365 / Entra ID using OIDC.\n* **SAML 2.0** – Use when users authenticate via a SAML provider that ultimately maps to Microsoft Entra ID.\nChoose the option that matches how your organization already authenticates users into Mattermost on other clients.",
|
||||
"admin.intune.authServiceOffice365": "OpenID Connect (Office 365)",
|
||||
"admin.intune.authServicePlaceholder": "Select an auth provider",
|
||||
"admin.intune.authServiceSaml": "SAML 2.0",
|
||||
"admin.intune.authServiceTitle": "Auth Provider:",
|
||||
"admin.intune.clientIdDescription": "The Application (Client) ID of your Intune MAM–enabled app registration in Microsoft Entra ID.\nThis is the client identifier that the Mattermost Mobile app uses to request MSAL tokens for Intune MAM enrollment and policy evaluation.",
|
||||
"admin.intune.clientIdPlaceholder": "E.g.: \"87654321-4321-4321-4321-210987654321\"",
|
||||
"admin.intune.clientIdTitle": "Application (Client) ID:",
|
||||
"admin.intune.enableDescription": "When enabled, Mattermost Mobile uses Microsoft Entra ID (Azure AD) for app authentication and policy enforcement. Users authenticate using MSAL tokens, and Intune MAM policies (App Protection Policies) are applied to protect corporate data.",
|
||||
"admin.intune.enableTitle": "Enable Microsoft Intune MAM:",
|
||||
"admin.intune.tenantIdDescription": "The Microsoft Entra ID (Azure AD) Tenant ID (also called the Directory ID).\nThis is the globally unique identifier that represents your organization in Microsoft Entra ID.\nMattermost uses this ID to validate tokens issued for Intune MAM.",
|
||||
"admin.intune.tenantIdPlaceholder": "E.g.: \"12345678-1234-1234-1234-123456789012\"",
|
||||
"admin.intune.tenantIdTitle": "Tenant ID:",
|
||||
"admin.ip_filtering.add_filter": "Add a filter",
|
||||
"admin.ip_filtering.add_ip_filter": "Add IP Filter",
|
||||
"admin.ip_filtering.add_your_ip": "Add your IP address",
|
||||
|
|
@ -1784,6 +1799,8 @@
|
|||
"admin.mobileSecurity.mobileAllowDownloads": "Site Configuration > File Sharing and Downloads > Allow File Downloads on Mobile",
|
||||
"admin.mobileSecurity.screenCaptureDescription": "Blocks screenshots and screen recordings when using the mobile app. Screenshots will appear blank, and screen recordings will blur (iOS) or show a black screen (Android). Also applies when switching apps.",
|
||||
"admin.mobileSecurity.screenCaptureTitle": "Prevent Screen Capture:",
|
||||
"admin.mobileSecurity.sections.general.description": "Configure device security features for the mobile app.",
|
||||
"admin.mobileSecurity.sections.intune.description": "Configure Microsoft Intune Mobile Application Management (MAM) for App Protection Policies.",
|
||||
"admin.mobileSecurity.secureFilePreviewDescription": "Prevents file downloads, previews, and sharing for most file types, even if {mobileAllowDownloads} is enabled. Allows in-app previews for PDFs, videos, and images only. Files are stored temporarily in the app’s cache and cannot be exported or shared.",
|
||||
"admin.mobileSecurity.secureFilePreviewTitle": "Enable Secure File Preview Mode:",
|
||||
"admin.mobileSecurity.title": "Mobile Security",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ describe('AdminConsoleIndex.generateIndex', () => {
|
|||
'authentication/saml',
|
||||
'environment/session_lengths',
|
||||
'authentication/email',
|
||||
'environment/mobile_security',
|
||||
'experimental/features',
|
||||
]);
|
||||
expect(idx.search('nginx')).toEqual([
|
||||
|
|
|
|||
|
|
@ -807,6 +807,14 @@ export type NativeAppSettings = {
|
|||
MobileJailbreakProtection: boolean;
|
||||
MobileEnableSecureFilePreview: boolean;
|
||||
MobileAllowPdfLinkNavigation: boolean;
|
||||
EnableIntuneMAM: boolean;
|
||||
};
|
||||
|
||||
export type IntuneSettings = {
|
||||
Enable: boolean;
|
||||
TenantId?: string;
|
||||
ClientId?: string;
|
||||
AuthService?: string;
|
||||
};
|
||||
|
||||
export type ClusterSettings = {
|
||||
|
|
@ -1052,6 +1060,7 @@ export type AdminConfig = {
|
|||
LocalizationSettings: LocalizationSettings;
|
||||
SamlSettings: SamlSettings;
|
||||
NativeAppSettings: NativeAppSettings;
|
||||
IntuneSettings: IntuneSettings;
|
||||
ClusterSettings: ClusterSettings;
|
||||
MetricsSettings: MetricsSettings;
|
||||
ExperimentalSettings: ExperimentalSettings;
|
||||
|
|
|
|||
Loading…
Reference in a new issue