From 5dc24375d0f414317d59c8c91cabd50fb9b064b2 Mon Sep 17 00:00:00 2001 From: Mathew Date: Mon, 10 Feb 2025 12:42:21 -0500 Subject: [PATCH] feat: adds users with admin privileges to home screen --- api/launchers/stig-manager.bat | 10 +++ api/launchers/stig-manager.sh | 10 +++ api/source/controllers/User.js | 3 +- api/source/index.js | 1 + api/source/service/UserService.js | 14 +++- api/source/specification/stig-manager.yaml | 10 +++ api/source/utils/config.js | 1 + api/source/utils/log-schema.json | 3 + client/build.sh | 1 + client/src/css/dark-mode.css | 4 + client/src/css/stigman.css | 60 ++++++++++++++- client/src/js/SM/FlexboxLayout.js | 13 ++++ client/src/js/SM/MainPanel.js | 90 +++++++++++++++++++--- client/src/js/resources.js | 1 + client/src/js/stigman.js | 36 ++++----- docs/installation-and-setup/envvars.csv | 2 + test/api/mocha/data/user/user.test.js | 27 +++++++ 17 files changed, 253 insertions(+), 33 deletions(-) create mode 100644 client/src/js/SM/FlexboxLayout.js diff --git a/api/launchers/stig-manager.bat b/api/launchers/stig-manager.bat index b78685786..8a52b1638 100644 --- a/api/launchers/stig-manager.bat +++ b/api/launchers/stig-manager.bat @@ -78,6 +78,16 @@ ::============================================================================== :: set STIGMAN_CLIENT_DISABLED= +::============================================================================== +:: STIGMAN_CLIENT_DISPLAY_APPMANAGERS +:: +:: | Default: "true" | Set to "false" to hide application manager names +:: along with their email addresses on the Stig-Manager home page. +:: +:: Affects: Client +::============================================================================== +:: set STIGMAN_CLIENT_DISPLAY_APPMANAGERS= + ::============================================================================== :: STIGMAN_CLIENT_EXTRA_SCOPES :: diff --git a/api/launchers/stig-manager.sh b/api/launchers/stig-manager.sh index e7e53453f..9cec2f28f 100755 --- a/api/launchers/stig-manager.sh +++ b/api/launchers/stig-manager.sh @@ -77,6 +77,16 @@ #============================================================================== # export STIGMAN_CLIENT_DISABLED= +#============================================================================== +# STIGMAN_CLIENT_DISPLAY_APPMANAGERS +# +# | Default: "true" | Set to "false" to hide application manager names +# along with their email addresses on the Stig-Manager home page. +# +# Affects: Client +#============================================================================== +# set STIGMAN_CLIENT_DISPLAY_APPMANAGERS= + #============================================================================== # STIGMAN_CLIENT_EXTRA_SCOPES # diff --git a/api/source/controllers/User.js b/api/source/controllers/User.js index 5e872a1c9..9b8293e97 100755 --- a/api/source/controllers/User.js +++ b/api/source/controllers/User.js @@ -120,11 +120,12 @@ module.exports.getUsers = async function getUsers (req, res, next) { let elevate = req.query.elevate let username = req.query.username let usernameMatch = req.query['username-match'] + let privilege = req.query['privilege'] let projection = req.query.projection if ( !elevate && projection?.length > 0) { throw new SmError.PrivilegeError() } - let response = await UserService.getUsers( username, usernameMatch, projection, elevate, req.userObject) + let response = await UserService.getUsers( username, usernameMatch, privilege, projection, elevate, req.userObject) res.json(response) } catch(err) { diff --git a/api/source/index.js b/api/source/index.js index 828300c85..f92b0909c 100644 --- a/api/source/index.js +++ b/api/source/index.js @@ -200,6 +200,7 @@ const STIGMAN = { Env: { version: "${config.version}", apiBase: "${config.client.apiBase}", + displayAppManagers: ${config.client.displayAppManagers}, welcome: { image: "${config.client.welcome.image}", title: "${config.client.welcome.title.replace(/"/g, '\\"')}", diff --git a/api/source/service/UserService.js b/api/source/service/UserService.js index 856b4d1e3..4ca3ba46a 100644 --- a/api/source/service/UserService.js +++ b/api/source/service/UserService.js @@ -109,6 +109,15 @@ exports.queryUsers = async function (inProjection, inPredicates, elevate, userOb predicates.statements.push(`ud.username ${matchStr}`) predicates.binds.push(inPredicates.username) } + + if (inPredicates.privilege) { + predicates.statements.push( + `JSON_CONTAINS(JSON_EXTRACT(ud.lastClaims, ?), ?) ` + ) + predicates.binds.push(`$.${config.oauth.claims.privileges}`, JSON.stringify([inPredicates.privilege])) + } + + if (needsCollectionGrantees) { ctes.push(dbUtils.sqlGrantees({userId: inPredicates.userId, username: inPredicates.username, returnCte: true})) } @@ -293,11 +302,12 @@ exports.getUserByUsername = async function(username, projection, elevate, userOb } } -exports.getUsers = async function(username, usernameMatch, projection, elevate, userObject) { +exports.getUsers = async function(username, usernameMatch, privilege, projection, elevate, userObject) { try { let rows = await _this.queryUsers( projection, { username: username, - usernameMatch: usernameMatch + usernameMatch: usernameMatch, + privilege: privilege }, elevate, userObject) return (rows) } diff --git a/api/source/specification/stig-manager.yaml b/api/source/specification/stig-manager.yaml index be42702f0..6371f5139 100644 --- a/api/source/specification/stig-manager.yaml +++ b/api/source/specification/stig-manager.yaml @@ -3855,6 +3855,7 @@ paths: operationId: getUsers parameters: - $ref: '#/components/parameters/UsernameQuery' + - $ref: '#/components/parameters/PrivilegeQuery' - $ref: '#/components/parameters/UsernameMatchQuery' responses: '200': @@ -8381,6 +8382,15 @@ components: - EMASS - MCCAST default: EMASS + PrivilegeQuery: + name: privilege + in: query + description: Selects Users with the specified privilege + schema: + type: string + enum: + - admin + - create_collection RetentionDateQuery: name: retentionDate in: query diff --git a/api/source/utils/config.js b/api/source/utils/config.js index 5e0145e4b..ec299da4a 100644 --- a/api/source/utils/config.js +++ b/api/source/utils/config.js @@ -18,6 +18,7 @@ const config = { }, client: { clientId: process.env.STIGMAN_CLIENT_ID || "stig-manager", + displayAppManagers: process.env.STIGMAN_CLIENT_DISPLAY_APPMANAGERS || "true", authority: process.env.STIGMAN_CLIENT_OIDC_PROVIDER || process.env.STIGMAN_OIDC_PROVIDER || "http://localhost:8080/realms/stigman", apiBase: process.env.STIGMAN_CLIENT_API_BASE || "api", disabled: process.env.STIGMAN_CLIENT_DISABLED === "true", diff --git a/api/source/utils/log-schema.json b/api/source/utils/log-schema.json index e0c210a47..87d4d9480 100644 --- a/api/source/utils/log-schema.json +++ b/api/source/utils/log-schema.json @@ -129,6 +129,9 @@ "clientId": { "type": "string" }, + "displayAppManagers": { + "type": "boolean" + }, "authority": { "type": "string" }, diff --git a/client/build.sh b/client/build.sh index 5bc264c14..3ae055547 100755 --- a/client/build.sh +++ b/client/build.sh @@ -111,6 +111,7 @@ uglifyjs \ 'SM/Global.js' \ 'SM/StackTrace.js' \ 'SM/Error.js' \ +'SM/FlexboxLayout.js' \ 'BufferView.js' \ 'SM/EventDispatcher.js' \ 'SM/Cache.js' \ diff --git a/client/src/css/dark-mode.css b/client/src/css/dark-mode.css index 061976502..9ad684d58 100644 --- a/client/src/css/dark-mode.css +++ b/client/src/css/dark-mode.css @@ -2369,6 +2369,10 @@ td.sort-asc, td.sort-desc, td.x-grid3-hd-menu-open, td.x-grid3-hd-over { background-color: #2b2e30 } +.sm-user-list { + background-color: #2b2e30; +} + .sm-home-widget-header { background-color: #313537 } diff --git a/client/src/css/stigman.css b/client/src/css/stigman.css index d8a2cdf53..7acf348e8 100644 --- a/client/src/css/stigman.css +++ b/client/src/css/stigman.css @@ -1110,11 +1110,59 @@ td.x-grid3-hd-over .x-grid3-hd-inner { border: none; margin: 10px; height: 400px; - width: 380px + width: 380px; } + +.sm-scroll-home-widget-body { + max-height: 340px; + overflow: auto; + scrollbar-width: thin; + margin-right: 8px; + margin-top: 5px; +} + +.sm-user-list { + list-style: none; + max-height: 340px; + padding : 0; + margin-left: 0; + +} + +.sm-user-item { + padding: 3px; + display: flex; + align-items: left; +} + +.sm-user-item::before { + content: "• "; + font-size: 16px; + color: #dedede; + margin-right: 3px; +} + +.sm-user-details { + display: flex; + flex-direction: column; + font-size: 13px; +} + +.sm-user-name { + font-size: 13px; + font-weight: bold; +} + +.sm-user-email { + font-size: 11px; + font-style: italic; +} + + .sm-home-widget-header { background-color: #d3d3d3 } + .sm-home-widget-title { padding: 10px 20px; font: bold 18px Open Sans,helvetica,sans-serif; @@ -2395,3 +2443,13 @@ td.x-grid3-hd-over .x-grid3-hd-inner { .sm-checklist-read { background-color: hsl(330 55% 24% / 1); } +.sm-flexbox-layout-ct { + display: flex; + flex-wrap: wrap; + overflow: auto; + scrollbar-width: none; + align-content: flex-start; +} +.sm-flexbox-layout-ct:hover { + scrollbar-width: auto; +} \ No newline at end of file diff --git a/client/src/js/SM/FlexboxLayout.js b/client/src/js/SM/FlexboxLayout.js new file mode 100644 index 000000000..6ccf39fd7 --- /dev/null +++ b/client/src/js/SM/FlexboxLayout.js @@ -0,0 +1,13 @@ +SM.FlexboxLayout = Ext.extend(Ext.layout.ContainerLayout, { + onLayout : function(ct, target){ + target.addClass('sm-flexbox-layout-ct'); + this.renderAll(ct, target); + }, + renderItem : function(c, position, target){ + if(c && !c.rendered){ + c.render(target); + this.configureItem(c); + } + } +}) +Ext.Container.LAYOUTS['sm-flexbox'] = SM.FlexboxLayout diff --git a/client/src/js/SM/MainPanel.js b/client/src/js/SM/MainPanel.js index 093c34f19..d10245498 100644 --- a/client/src/js/SM/MainPanel.js +++ b/client/src/js/SM/MainPanel.js @@ -90,18 +90,22 @@ SM.WelcomeWidget = Ext.extend(Ext.Panel, { `
`, `
Welcome
`, `
`, - `
`, - `
`, - ``, - `
`, - `STIG Manager is an API and Web client for managing the assessment of Information Systems for compliance with security checklists published by the United States Defense Information Systems Agency (DISA). The software is an open source project maintained by the Naval Sea Systems Command (NAVSEA) of the United States Navy.`, - `
`, - `
`, - `
${STIGMAN.Env.welcome.title ? SM.he(STIGMAN.Env.welcome.title) : STIGMAN.Env.welcome.message || STIGMAN.Env.welcome.link ? 'Support' : ''}
`, - `${SM.he(STIGMAN.Env.welcome.message)}${STIGMAN.Env.welcome.message && STIGMAN.Env.welcome.link ? '

' : ''}`, - `${STIGMAN.Env.welcome.link ? '' + STIGMAN.Env.welcome.link + '': ''}`, - `
` + `
`, + `
`, + `
`, + ``, + `
`, + `STIG Manager is an API and Web client for managing the assessment of Information Systems for compliance with security checklists published by the United States Defense Information Systems Agency (DISA). The software is an open source project maintained by the Naval Sea Systems Command (NAVSEA) of the United States Navy.`, + `
`, + `
`, + `
+ ${STIGMAN.Env.welcome.title ? SM.he(STIGMAN.Env.welcome.title) : STIGMAN.Env.welcome.message || STIGMAN.Env.welcome.link ? 'Support' : ''} +
`, + `${SM.he(STIGMAN.Env.welcome.message)}${STIGMAN.Env.welcome.message && STIGMAN.Env.welcome.link ? '

' : ''}`, + `${STIGMAN.Env.welcome.link ? '' + STIGMAN.Env.welcome.link + '': ''}`, + `
`, + `
`, ) const config = { tpl: tpl, @@ -200,6 +204,70 @@ SM.ResourcesWidget = Ext.extend(Ext.Panel, { }) Ext.reg('sm-home-widget-resources', SM.ResourcesWidget) +SM.ApplicationManagers = Ext.extend(Ext.Panel, { + initComponent: function() { + const me = this + me.userListId = Ext.id() + + const tpl = new Ext.XTemplate( + `
`, + `
+ Application Managers +
`, + `
`, + `
`, + `
`, + `
+
`, + `
`, + `
` + + ) + const config = { + tpl: tpl, + bodyCssClass: 'sm-home-widget-body', + border: false, + data: {}, + autoScroll: false + } + Ext.apply(this, Ext.apply(this.initialConfig, config)) + SM.ApplicationManagers.superclass.initComponent.call(this) + this.on('afterrender', this.loadApplicationManagers, this) + }, + loadApplicationManagers: async function () { + const me = this; + try { + const response = await Ext.Ajax.requestPromise({ + responseType: 'json', + url: `${STIGMAN.Env.apiBase}/users?privilege=admin`, + method: 'GET', + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + }) + const userList = Ext.get(me.userListId) + if (userList) { + const userItems = response + .map((user) => { + return ` +
  • +
    + ${user.displayName} + ${user.email + ? `${user.email}` + : `No Email Available`} +
    +
  • ` + }) + .join('') + userList.update(``) + } + + } catch (e) { + SM.Error.handleError(e) + } + } +}) +Ext.reg('sm-home-widget-app-managers', SM.ApplicationManagers) + SM.StigWidget = Ext.extend(Ext.Panel, { initComponent: function() { const me = this diff --git a/client/src/js/resources.js b/client/src/js/resources.js index e0ee03421..aa13f6564 100644 --- a/client/src/js/resources.js +++ b/client/src/js/resources.js @@ -20,6 +20,7 @@ const scripts = [ 'js/stigmanUtils.js', 'js/SM/Global.js', 'js/SM/Error.js', + 'js/SM/FlexboxLayout.js', 'js/BufferView.js', 'js/SM/EventDispatcher.js', 'js/SM/Cache.js', diff --git a/client/src/js/stigman.js b/client/src/js/stigman.js index 86181bfe3..92608dd29 100644 --- a/client/src/js/stigman.js +++ b/client/src/js/stigman.js @@ -113,35 +113,35 @@ async function loadApp () { const appTitleHtml = `
    STIG ManagerOSS${STIGMAN.Env.version}
    ` - const homeTab = new SM.HomeTab({ border: false, region: 'center', - layout: 'table', + layout: 'vbox', layoutConfig: { - tableAttrs: { - style: { - padding: '20px', - "table-layout": 'fixed' - - } - }, - columns: 3 + align: 'stretch', }, items: [ { html: appTitleHtml, - colspan: 3, + height: 80, border: false }, { - xtype: 'sm-home-widget-welcome' - }, - { - xtype: 'sm-home-widget-doc' - }, - { - xtype: 'sm-home-widget-resources' + layout: 'sm-flexbox', + flex: 1, + border: false, + items: [ + { + xtype: 'sm-home-widget-welcome' + }, + { + xtype: 'sm-home-widget-doc' + }, + { + xtype: 'sm-home-widget-resources' + }, + ...(STIGMAN.Env.displayAppManagers ? [{ xtype: 'sm-home-widget-app-managers' }] : []) + ] } ] }) diff --git a/docs/installation-and-setup/envvars.csv b/docs/installation-and-setup/envvars.csv index 0f933f043..ac5cba742 100644 --- a/docs/installation-and-setup/envvars.csv +++ b/docs/installation-and-setup/envvars.csv @@ -15,6 +15,8 @@ | The location of the web client files, relative to the API source directory. Note that if running source from a clone of the GitHub repository, the client is located at `../../clients` relative to the API directory. ","API, Client" "STIGMAN_CLIENT_DISABLED","| **Default** ``false`` | Whether to *not* serve the reference web client","Client" +"STIGMAN_CLIENT_DISPLAY_APPMANAGERS","| **Default** ``true`` +| Whether to display application managers the home page of web client","Client" "STIGMAN_CLIENT_EXTRA_SCOPES","| **No default** | A space separated list of OAuth2 scopes to request in addition to ``stig-manager:stig`` ``stig-manager:stig:read`` ``stig-manager:collection`` ``stig-manager:user`` ``stig-manager:user:read`` ``stig-manager:op``. Some OIDC providers (Okta) generate a refresh token only if the scope ``offline_access`` is requested","Client" "STIGMAN_CLIENT_ID","| **Default** ``stig-manager`` diff --git a/test/api/mocha/data/user/user.test.js b/test/api/mocha/data/user/user.test.js index 9cef29852..09072f55e 100644 --- a/test/api/mocha/data/user/user.test.js +++ b/test/api/mocha/data/user/user.test.js @@ -184,6 +184,33 @@ describe('user', () => { expect(res.body[0].userGroups, "expect user to be in TestGroup").to.eql([{ userGroupId: reference.testCollection.testGroup.userGroupId, name: reference.testCollection.testGroup.name }]) }) + it("should return all users with admin privileges", async () => { + + const res = await utils.executeRequest(`${config.baseUrl}/users?elevate=true&privilege=admin`, 'GET', iteration.token) + + if(iteration.name != "stigmanadmin"){ + expect(res.status).to.eql(403) + return + } + expect(res.status).to.eql(200) + + for(const user of res.body) { + expect(user.privileges.admin, "expect user to have admin privilege").to.be.true + } + }) + it("should return all users with create_collection privileges", async () => { + + const res = await utils.executeRequest(`${config.baseUrl}/users?elevate=true&privilege=create_collection`, 'GET', iteration.token) + + if(iteration.name != "stigmanadmin"){ + expect(res.status).to.eql(403) + return + } + expect(res.status).to.eql(200) + for(const user of res.body) { + expect(user.privileges.create_collection, "expect user to have create_collection privilege").to.be.true + } + }) it("should throw SmError.PrivilegeError no elevate with projections.", async () => { const res = await utils.executeRequest(`${config.baseUrl}/users?projection=collectionGrants`, 'GET', iteration.token) expect(res.status).to.eql(403)