diff --git a/package.json b/package.json index 5bdf13c020631..d6f2bffd002e8 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "url": "https://github.com/elastic/kibana.git" }, "dependencies": { - "@elastic/eui": "v0.0.47", + "@elastic/eui": "v0.0.51", "@elastic/filesaver": "1.1.2", "@elastic/numeral": "2.3.2", "@elastic/ui-ace": "0.2.3", @@ -205,8 +205,8 @@ "validate-npm-package-name": "2.2.2", "vega-lib": "^3.3.1", "vega-lite": "^2.4.0", - "vega-tooltip": "^0.9.14", "vega-schema-url-parser": "1.0.0", + "vega-tooltip": "^0.9.14", "vision": "4.1.0", "webpack": "3.6.0", "webpack-merge": "4.1.0", diff --git a/packages/kbn-ui-framework/dist/ui_framework.css b/packages/kbn-ui-framework/dist/ui_framework.css index 0d013535e3555..101f718149647 100644 --- a/packages/kbn-ui-framework/dist/ui_framework.css +++ b/packages/kbn-ui-framework/dist/ui_framework.css @@ -78,15 +78,12 @@ main { overflow: hidden; } .kuiActionItem { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; } @@ -105,15 +102,12 @@ main { background-color: rgba(0, 0, 0, 0.1); } .kuiBar { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -121,22 +115,18 @@ main { /* 1 */ } .kuiBarSection { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; margin-left: 25px; margin-right: 25px; } .kuiBarSection:not(:first-child):not(:last-child):not(:only-child) { - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -145,12 +135,10 @@ main { margin-left: 0; } .kuiBarSection:last-child { margin-right: 0; - -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; /* 4 */ - -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -201,12 +189,10 @@ main { * 1. Solves whitespace problems introduced by inline elements. */ .kuiButton__inner { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; /* 1 */ - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -513,11 +499,9 @@ main { background-color: rgba(165, 231, 255, 0.5); } .kuiButtonGroup { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } @@ -545,24 +529,19 @@ main { margin-left: 2px; } .kuiButtonGroup--fullWidth { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } .kuiButtonGroup--fullWidth > .kuiButton { - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; text-align: center; } .kuiCard { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; @@ -572,20 +551,15 @@ main { line-height: 1.5; } .kuiCard__description { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: start; -webkit-justify-content: flex-start; -ms-flex-pack: start; justify-content: flex-start; @@ -613,7 +587,6 @@ main { * 2. Offset the spacing between wrapped cards. */ .kuiCardGroup { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; @@ -628,7 +601,6 @@ main { * 2. Use an even margin all around the card so that the spacing is still even when wrapped. */ } .kuiCardGroup .kuiCard { - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; @@ -636,7 +608,6 @@ main { margin: 15px; /* 2 */ } .kuiCardGroup .kuiCard .kuiCard__description { - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; } @@ -687,20 +658,15 @@ main { right: 0; left: 0; background: rgba(255, 255, 255, 0.7); - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -736,11 +702,9 @@ main { cursor: pointer; } .kuiColorPicker__preview { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } @@ -890,11 +854,9 @@ main { margin-right: 8px; } .kuiContextMenu__itemLayout { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } @@ -1071,13 +1033,11 @@ main { color: #ffffff; } .kuiContextMenuItem__inner { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } .kuiContextMenuItem__text { - -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; flex-grow: 1; } @@ -1096,13 +1056,11 @@ main { text-decoration: none; } .kuiEvent { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } .kuiEventSymbol { - -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; @@ -1111,7 +1069,6 @@ main { padding-right: 8px; } .kuiEventBody { - -webkit-box-flex: 1; -webkit-flex: 1 1 0%; -ms-flex: 1 1 0%; flex: 1 1 0%; } @@ -1150,16 +1107,13 @@ main { border-bottom: solid 2px #00A69B; } .kuiFlexGroup { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } .kuiFlexGroup .kuiFlexItem { - -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; flex-grow: 1; } @@ -1185,13 +1139,11 @@ main { margin: 20px; } .kuiFlexGroup--justifyContentSpaceEvenly { - -webkit-box-pack: space-evenly; -webkit-justify-content: space-evenly; -ms-flex-pack: space-evenly; justify-content: space-evenly; } .kuiFlexGroup--justifyContentSpaceBetween { - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; } @@ -1202,31 +1154,26 @@ main { justify-content: space-around; } .kuiFlexGroup--justifyContentCenter { - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; } .kuiFlexGroup--justifyContentFlexEnd { - -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; } .kuiFlexGroup--alignItemsStart { - -webkit-box-align: start; -webkit-align-items: flex-start; -ms-flex-align: start; align-items: flex-start; } .kuiFlexGroup--alignItemsCenter { - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } .kuiFlexGroup--alignItemsEnd { - -webkit-box-align: end; -webkit-align-items: flex-end; -ms-flex-align: end; align-items: flex-end; } @@ -1243,7 +1190,6 @@ main { flex-wrap: wrap; } } .kuiFlexGrid { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; @@ -1252,12 +1198,10 @@ main { flex-wrap: wrap; margin-bottom: 0; } .kuiFlexGrid > .kuiFlexItem { - -webkit-box-flex: 0; -webkit-flex-grow: 0; -ms-flex-positive: 0; flex-grow: 0; } .kuiFlexGrid > .kuiFlexItem.kuiFlexItem--flexGrowZero { - -webkit-box-flex: 0 !important; -webkit-flex-grow: 0 !important; -ms-flex-positive: 0 !important; flex-grow: 0 !important; @@ -1274,7 +1218,6 @@ main { .kuiFlexGrid--gutterSmall { margin: -4px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1285,7 +1228,6 @@ main { .kuiFlexGrid--gutterSmall { margin: -4px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1296,7 +1238,6 @@ main { .kuiFlexGrid--gutterSmall { margin: -4px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1314,7 +1255,6 @@ main { .kuiFlexGrid--gutterMedium { margin: -8px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1325,7 +1265,6 @@ main { .kuiFlexGrid--gutterMedium { margin: -8px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1336,7 +1275,6 @@ main { .kuiFlexGrid--gutterMedium { margin: -8px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1354,7 +1292,6 @@ main { .kuiFlexGrid--gutterLarge { margin: -12px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1365,7 +1302,6 @@ main { .kuiFlexGrid--gutterLarge { margin: -12px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1376,7 +1312,6 @@ main { .kuiFlexGrid--gutterLarge { margin: -12px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1394,7 +1329,6 @@ main { .kuiFlexGrid--gutterXLarge { margin: -16px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1405,7 +1339,6 @@ main { .kuiFlexGrid--gutterXLarge { margin: -16px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1416,7 +1349,6 @@ main { .kuiFlexGrid--gutterXLarge { margin: -16px; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -1429,13 +1361,10 @@ main { * 1. Allow KuiPanels to expand to fill the item. */ .kuiFlexItem { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; /* 1 */ - -webkit-box-orient: vertical; - -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; @@ -1446,7 +1375,6 @@ main { */ } .kuiFlexItem.kuiFlexItem--flexGrowZero { /* 1 */ - -webkit-box-flex: 0; -webkit-flex-grow: 0; -ms-flex-positive: 0; flex-grow: 0; @@ -1456,52 +1384,42 @@ main { flex-basis: auto; /* 2 */ } .kuiFlexItem.kuiFlexItem--flexGrow1 { - -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; flex-grow: 1; } .kuiFlexItem.kuiFlexItem--flexGrow2 { - -webkit-box-flex: 2; -webkit-flex-grow: 2; -ms-flex-positive: 2; flex-grow: 2; } .kuiFlexItem.kuiFlexItem--flexGrow3 { - -webkit-box-flex: 3; -webkit-flex-grow: 3; -ms-flex-positive: 3; flex-grow: 3; } .kuiFlexItem.kuiFlexItem--flexGrow4 { - -webkit-box-flex: 4; -webkit-flex-grow: 4; -ms-flex-positive: 4; flex-grow: 4; } .kuiFlexItem.kuiFlexItem--flexGrow5 { - -webkit-box-flex: 5; -webkit-flex-grow: 5; -ms-flex-positive: 5; flex-grow: 5; } .kuiFlexItem.kuiFlexItem--flexGrow6 { - -webkit-box-flex: 6; -webkit-flex-grow: 6; -ms-flex-positive: 6; flex-grow: 6; } .kuiFlexItem.kuiFlexItem--flexGrow7 { - -webkit-box-flex: 7; -webkit-flex-grow: 7; -ms-flex-positive: 7; flex-grow: 7; } .kuiFlexItem.kuiFlexItem--flexGrow8 { - -webkit-box-flex: 8; -webkit-flex-grow: 8; -ms-flex-positive: 8; flex-grow: 8; } .kuiFlexItem.kuiFlexItem--flexGrow9 { - -webkit-box-flex: 9; -webkit-flex-grow: 9; -ms-flex-positive: 9; flex-grow: 9; } .kuiFlexItem.kuiFlexItem--flexGrow10 { - -webkit-box-flex: 10; -webkit-flex-grow: 10; -ms-flex-positive: 10; flex-grow: 10; } @@ -1592,11 +1510,9 @@ main { background-color: #0079a5; } .kuiCheckBoxLabel { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -1860,18 +1776,15 @@ main { * 1. We may want to put elements in here which have different heights. */ .kuiFieldGroup { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; /* 1 */ } .kuiFieldGroup--alignTop { - -webkit-box-align: start; -webkit-align-items: flex-start; -ms-flex-align: start; align-items: flex-start; } @@ -1882,7 +1795,6 @@ main { margin-left: 10px; } .kuiFieldGroupSection--wide { - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; } @@ -1890,7 +1802,6 @@ main { width: 100%; } .kuiGallery { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; @@ -1899,20 +1810,15 @@ main { flex-wrap: wrap; } .kuiGalleryItem { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -1931,15 +1837,12 @@ main { border-color: #00A6FF; } .kuiGalleryItem__image { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -1970,15 +1873,12 @@ main { color: #666; } .kuiHeaderBar { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -1990,22 +1890,18 @@ main { * 1. Align a single section to the left by default. */ .kuiHeaderBarSection { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; margin-left: 25px; margin-right: 25px; } .kuiHeaderBarSection:not(:first-child):not(:last-child):not(:only-child) { - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -2014,12 +1910,10 @@ main { margin-left: 0; } .kuiHeaderBarSection:last-child { margin-right: 0; - -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; /* 4 */ - -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -2118,11 +2012,9 @@ main { * 1. Align with first line of title text if it wraps. */ .kuiInfoPanelHeader { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: baseline; -webkit-align-items: baseline; -ms-flex-align: baseline; align-items: baseline; @@ -2179,11 +2071,9 @@ main { * a bit. */ .kuiLocalBreadcrumbs { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -2253,15 +2143,12 @@ main { padding: 0; } .kuiDatePickerNavigation { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -2425,13 +2312,11 @@ main { /* 1 */ } .kuiLocalDropdownPanels { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } .kuiLocalDropdownPanel { - -webkit-box-flex: 1; -webkit-flex: 1 1 0%; -ms-flex: 1 1 0%; flex: 1 1 0%; } @@ -2460,15 +2345,12 @@ main { margin-bottom: 0; } .kuiLocalDropdownHeader { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -2487,7 +2369,6 @@ main { color: #cecece; } .kuiLocalDropdownHeader__actions { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } @@ -2549,21 +2430,17 @@ main { color: #9e9e9e; } .kuiLocalMenu { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } .kuiLocalMenuItem { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -2615,16 +2492,12 @@ main { * dropdown. */ .kuiLocalNav { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -2643,15 +2516,12 @@ main { * 1. Allow row to expand if the content is so long that it wraps. */ .kuiLocalNavRow { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -2659,11 +2529,9 @@ main { /* 1 */ } .kuiLocalNavRow__section { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } @@ -2678,14 +2546,12 @@ main { .kuiLocalNavRow--secondary { padding: 0 10px; /* 1 */ - -webkit-box-align: start; -webkit-align-items: flex-start; -ms-flex-align: start; align-items: flex-start; /* 1 */ } .kuiLocalSearch { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; @@ -2708,7 +2574,6 @@ main { transition: border-color 0.1s linear; min-height: 30px; /* 1 */ - -webkit-box-flex: 1; -webkit-flex: 1 1 100%; -ms-flex: 1 1 100%; flex: 1 1 100%; @@ -2738,7 +2603,6 @@ main { .kuiLocalSearchInput--secondary { height: 30px; - -webkit-box-flex: 0; -webkit-flex: 0 0 auto; -ms-flex: 0 0 auto; flex: 0 0 auto; @@ -2750,11 +2614,9 @@ main { border-right-color: #333333; } .kuiLocalSearchAssistedInput { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-flex: 1; -webkit-flex: 1 1 100%; -ms-flex: 1 1 100%; flex: 1 1 100%; @@ -2857,11 +2719,9 @@ main { * 1. We want the bottom border on selected tabs to be flush with the bottom of the container. */ .kuiLocalTabs { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: end; -webkit-align-items: flex-end; -ms-flex-align: end; align-items: flex-end; @@ -2906,11 +2766,9 @@ main { color: #dedede; } .kuiLocalTitle { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -3042,7 +2900,6 @@ main { /* 3 */ } .kuiMenuButtonGroup { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } @@ -3050,7 +2907,6 @@ main { margin-left: 4px; } .kuiMenuButtonGroup--alignRight { - -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; } @@ -3073,7 +2929,6 @@ main { color: #191E23; } .kuiMicroButtonGroup { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } @@ -3087,15 +2942,12 @@ main { left: 0; right: 0; bottom: 0; - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -3121,15 +2973,12 @@ main { min-width: auto; } .kuiModalHeader { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -3170,11 +3019,9 @@ main { color: #cecece; } .kuiModalFooter { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -3213,11 +3060,9 @@ main { * 1. Put 10px of space between each child. */ .kuiPager { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } @@ -3237,21 +3082,16 @@ main { border-radius: 4px; } .kuiPanel--prompt { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; text-align: center; - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -3268,29 +3108,23 @@ main { border-radius: 0; } .kuiPanel--centered { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } .kuiPanelHeader { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -3352,22 +3186,18 @@ main { * 1. Undo what barSection mixin does. */ .kuiPanelHeaderSection { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; margin-left: 25px; margin-right: 25px; } .kuiPanelHeaderSection:not(:first-child):not(:last-child):not(:only-child) { - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -3376,12 +3206,10 @@ main { margin-left: 0; } .kuiPanelHeaderSection:last-child { margin-right: 0; - -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; /* 4 */ - -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -3406,7 +3234,6 @@ main { background-color: #FFF; border: 1px solid #D9D9D9; border-radius: 4px; - -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; flex-grow: 1; } @@ -3419,7 +3246,6 @@ main { .kuiPanelSimple.kuiPanelSimple--shadow { box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); } .kuiPanelSimple.kuiPanelSimple--flexGrowZero { - -webkit-box-flex: 0; -webkit-flex-grow: 0; -ms-flex-positive: 0; flex-grow: 0; } @@ -3513,16 +3339,12 @@ main { color: #ffffff; } .kuiEmptyTablePrompt { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; @@ -3537,11 +3359,9 @@ main { margin-top: 10px; } .kuiStatusText { - display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; display: inline-flex; - -webkit-box-align: baseline; -webkit-align-items: baseline; -ms-flex-align: baseline; align-items: baseline; } @@ -3657,11 +3477,9 @@ main { display: block; opacity: 1; } .kuiTableHeaderCellButton .kuiTableHeaderCell__liner { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } @@ -3762,7 +3580,6 @@ main { line-height: 1.5; } .kuiTabs { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; @@ -3886,15 +3703,12 @@ main { /* 1 */ } .kuiToolBar { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -3945,22 +3759,18 @@ main { border-color: #0079a5; } .kuiToolBarSection { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; margin-left: 25px; margin-right: 25px; } .kuiToolBarSection:not(:first-child):not(:last-child):not(:only-child) { - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -3969,12 +3779,10 @@ main { margin-left: 0; } .kuiToolBarSection:last-child { margin-right: 0; - -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; /* 4 */ - -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -3994,15 +3802,12 @@ main { /* 1 */ } .kuiToolBarFooter { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; @@ -4014,22 +3819,18 @@ main { border: 1px solid #D9D9D9; } .kuiToolBarFooterSection { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; margin-left: 25px; margin-right: 25px; } .kuiToolBarFooterSection:not(:first-child):not(:last-child):not(:only-child) { - -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; @@ -4038,12 +3839,10 @@ main { margin-left: 0; } .kuiToolBarFooterSection:last-child { margin-right: 0; - -webkit-box-flex: 0; -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; /* 4 */ - -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; @@ -4061,17 +3860,14 @@ main { * kuiToolBarSection sibling. */ .kuiToolBarSearch { - display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; margin-left: 25px; margin-right: 25px; - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; @@ -4087,7 +3883,6 @@ main { /* 1 */ } .kuiToolBarSearchBox { - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; @@ -4210,7 +4005,6 @@ main { .kuiView { background-color: #FFF; - -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; } diff --git a/x-pack/package.json b/x-pack/package.json index bfcb1dd5a9905..95aa06158b637 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -75,7 +75,7 @@ "yargs": "4.7.1" }, "dependencies": { - "@elastic/eui": "0.0.47", + "@elastic/eui": "v0.0.51", "@elastic/node-crypto": "0.1.2", "@elastic/node-phantom-simple": "2.2.4", "@elastic/numeral": "2.3.2", diff --git a/x-pack/plugins/security/public/documentation_links.js b/x-pack/plugins/security/public/documentation_links.js index 8d9bb3c2256b4..d357451d48ac7 100644 --- a/x-pack/plugins/security/public/documentation_links.js +++ b/x-pack/plugins/security/public/documentation_links.js @@ -7,5 +7,8 @@ import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; export const documentationLinks = { - dashboardViewMode: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-view-modes.html` + dashboardViewMode: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-view-modes.html`, + esClusterPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/x-pack/${DOC_LINK_VERSION}/security-privileges.html#security-privileges`, + esIndicesPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/x-pack/${DOC_LINK_VERSION}/security-privileges.html#privileges-list-indices`, + esRunAsPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/x-pack/${DOC_LINK_VERSION}/security-privileges.html#_run_as_privilege`, }; diff --git a/x-pack/plugins/security/public/lib/__tests__/role.js b/x-pack/plugins/security/public/lib/__tests__/role.js deleted file mode 100644 index efb22152ef7b1..0000000000000 --- a/x-pack/plugins/security/public/lib/__tests__/role.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from 'expect.js'; -import { isRoleEnabled } from '../role'; - -describe('role', () => { - describe('isRoleEnabled', () => { - it('should return false if role is explicitly not enabled', () => { - const testRole = { - transient_metadata: { - enabled: false - } - }; - expect(isRoleEnabled(testRole)).to.be(false); - }); - - it('should return true if role is explicitly enabled', () => { - const testRole = { - transient_metadata: { - enabled: true - } - }; - expect(isRoleEnabled(testRole)).to.be(true); - }); - - it('should return true if role is NOT explicitly enabled or disabled', () => { - const testRole = {}; - expect(isRoleEnabled(testRole)).to.be(true); - }); - }); -}); diff --git a/x-pack/plugins/security/public/lib/role.js b/x-pack/plugins/security/public/lib/role.js index 89eade0f0584e..96057aa55f595 100644 --- a/x-pack/plugins/security/public/lib/role.js +++ b/x-pack/plugins/security/public/lib/role.js @@ -14,4 +14,13 @@ import { get } from 'lodash'; */ export function isRoleEnabled(role) { return get(role, 'transient_metadata.enabled', true); -} \ No newline at end of file +} + +/** + * Returns whether given role is reserved or not. + * + * @param {role} the Role as returned by roles API + */ +export function isReservedRole(role) { + return get(role, 'metadata._reserved', false); +} diff --git a/x-pack/plugins/security/public/lib/role.test.js b/x-pack/plugins/security/public/lib/role.test.js new file mode 100644 index 0000000000000..4c6e9fb896dcf --- /dev/null +++ b/x-pack/plugins/security/public/lib/role.test.js @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isRoleEnabled, isReservedRole } from './role'; + +describe('role', () => { + describe('isRoleEnabled', () => { + test('should return false if role is explicitly not enabled', () => { + const testRole = { + transient_metadata: { + enabled: false + } + }; + expect(isRoleEnabled(testRole)).toBe(false); + }); + + test('should return true if role is explicitly enabled', () => { + const testRole = { + transient_metadata: { + enabled: true + } + }; + expect(isRoleEnabled(testRole)).toBe(true); + }); + + test('should return true if role is NOT explicitly enabled or disabled', () => { + const testRole = {}; + expect(isRoleEnabled(testRole)).toBe(true); + }); + }); + + describe('isReservedRole', () => { + test('should return false if role is explicitly not reserved', () => { + const testRole = { + metadata: { + _reserved: false + } + }; + expect(isReservedRole(testRole)).toBe(false); + }); + + test('should return true if role is explicitly reserved', () => { + const testRole = { + metadata: { + _reserved: true + } + }; + expect(isReservedRole(testRole)).toBe(true); + }); + + test('should return false if role is NOT explicitly reserved or not reserved', () => { + const testRole = {}; + expect(isReservedRole(testRole)).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/security/public/objects/index.js b/x-pack/plugins/security/public/objects/index.js new file mode 100644 index 0000000000000..a6238ca879901 --- /dev/null +++ b/x-pack/plugins/security/public/objects/index.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { saveRole, deleteRole } from './lib/roles'; + +export { getFields } from './lib/get_fields'; diff --git a/x-pack/plugins/security/public/objects/lib/get_fields.js b/x-pack/plugins/security/public/objects/lib/get_fields.js new file mode 100644 index 0000000000000..a80f6bfe8eed1 --- /dev/null +++ b/x-pack/plugins/security/public/objects/lib/get_fields.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import chrome from 'ui/chrome'; + +const apiBase = chrome.addBasePath(`/api/security/v1/fields`); + +export async function getFields($http, query) { + return await $http + .get(`${apiBase}/${query}`) + .then(response => response.data || []); +} diff --git a/x-pack/plugins/security/public/objects/lib/roles.js b/x-pack/plugins/security/public/objects/lib/roles.js new file mode 100644 index 0000000000000..5fdd95a7ccb64 --- /dev/null +++ b/x-pack/plugins/security/public/objects/lib/roles.js @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import chrome from 'ui/chrome'; + +const apiBase = chrome.addBasePath(`/api/security/v1/roles`); + +export async function saveRole($http, role) { + return await $http.post(`${apiBase}/${role.name}`, role); +} + +export async function deleteRole($http, name) { + return await $http.delete(`${apiBase}/${name}`); +} diff --git a/x-pack/plugins/security/public/objects/role.js b/x-pack/plugins/security/public/objects/role.js new file mode 100644 index 0000000000000..98448db207cea --- /dev/null +++ b/x-pack/plugins/security/public/objects/role.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class Role { + name = null; + cluster = []; + indices = []; + run_as = []; //eslint-disable-line camelcase + applications = []; +} diff --git a/x-pack/plugins/security/public/services/role_privileges.js b/x-pack/plugins/security/public/services/role_privileges.js new file mode 100644 index 0000000000000..794a4b30674e5 --- /dev/null +++ b/x-pack/plugins/security/public/services/role_privileges.js @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const clusterPrivileges = [ + 'all', + 'monitor', + 'manage', + 'manage_security', + 'manage_index_templates', + 'manage_pipeline', + 'manage_ingest_pipelines', + 'transport_client', + 'manage_ml', + 'monitor_ml', + 'manage_watcher', + 'monitor_watcher', +]; +const indexPrivileges = [ + 'all', + 'manage', + 'monitor', + 'read', + 'index', + 'create', + 'delete', + 'write', + 'delete_index', + 'create_index', + 'view_index_metadata', + 'read_cross_cluster', +]; + +export function getClusterPrivileges() { + return [...clusterPrivileges]; +} + +export function getIndexPrivileges() { + return [...indexPrivileges]; +} diff --git a/x-pack/plugins/security/public/views/management/edit_role.html b/x-pack/plugins/security/public/views/management/edit_role.html deleted file mode 100644 index 8c8c5d692cce2..0000000000000 --- a/x-pack/plugins/security/public/views/management/edit_role.html +++ /dev/null @@ -1,194 +0,0 @@ - - -
- - -
- -
- -

- - New Role - - - “{{ role.name }}” Role - -

-
- -
- - -
- - Reserved -
-
-
- -
-
-
- - - This role contains application privileges for the {{ otherApplications.join(', ') }} application(s) that can't be edited. - If they are for other instances of Kibana, you must manage those privileges on that Kibana. - -
-
-
- - -
- -
- - - - -
- Name must begin with a letter or underscore and contain only letters, underscores, and numbers. -
- -
- Name is required. -
-
- -
- -
- -
- -
-
- - -
- - -
- -
-
- - -
- - - - {{$item}} - - -
-
-
-
- - -
- -
-
- -
- -
- - - - Cancel - -
-
-
-
-
diff --git a/x-pack/plugins/security/public/views/management/edit_role.js b/x-pack/plugins/security/public/views/management/edit_role.js deleted file mode 100644 index 3f0ab02129dbb..0000000000000 --- a/x-pack/plugins/security/public/views/management/edit_role.js +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import routes from 'ui/routes'; -import { fatalError, toastNotifications } from 'ui/notify'; -import { toggle } from 'plugins/security/lib/util'; -import { isRoleEnabled } from 'plugins/security/lib/role'; -import template from 'plugins/security/views/management/edit_role.html'; -import 'angular-ui-select'; -import 'plugins/security/services/application_privilege'; -import 'plugins/security/services/shield_user'; -import 'plugins/security/services/shield_role'; -import 'plugins/security/services/shield_privileges'; -import 'plugins/security/services/shield_indices'; - -import { IndexPatternsProvider } from 'ui/index_patterns/index_patterns'; -import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; -import { checkLicenseError } from 'plugins/security/lib/check_license_error'; -import { EDIT_ROLES_PATH, ROLES_PATH } from './management_urls'; -import { DEFAULT_RESOURCE } from '../../../common/constants'; - -const getKibanaPrivileges = (kibanaApplicationPrivilege, role, application) => { - const kibanaPrivileges = kibanaApplicationPrivilege.reduce((acc, p) => { - acc[p.name] = false; - return acc; - }, {}); - - if (!role.applications || role.applications.length === 0) { - return kibanaPrivileges; - } - - const applications = role.applications.filter(x => x.application === application); - - const assigned = _.uniq(_.flatten(_.pluck(applications, 'privileges'))); - assigned.forEach(a => { - kibanaPrivileges[a] = true; - }); - - return kibanaPrivileges; -}; - -const setApplicationPrivileges = (kibanaPrivileges, role, application) => { - if (!role.applications) { - role.applications = []; - } - - // we first remove the matching application entries - role.applications = role.applications.filter(x => { - return x.application !== application; - }); - - const privileges = Object.keys(kibanaPrivileges).filter(key => kibanaPrivileges[key]); - - // if we still have them, put the application entry back - if (privileges.length > 0) { - role.applications = [...role.applications, { - application, - privileges, - resources: [ DEFAULT_RESOURCE ] - }]; - } -}; - -const getOtherApplications = (kibanaPrivileges, role, application) => { - if (!role.applications || role.applications.length === 0) { - return []; - } - - return role.applications.map(x => x.application).filter(x => x !== application); -}; - -routes.when(`${EDIT_ROLES_PATH}/:name?`, { - template, - resolve: { - role($route, ShieldRole, kbnUrl, Promise, Notifier) { - const name = $route.current.params.name; - if (name != null) { - return ShieldRole.get({ name }).$promise - .catch((response) => { - - if (response.status !== 404) { - return fatalError(response); - } - - const notifier = new Notifier(); - notifier.error(`No "${name}" role found.`); - kbnUrl.redirect(ROLES_PATH); - return Promise.halt(); - }); - } - return new ShieldRole({ - cluster: [], - indices: [], - run_as: [], - applications: [] - }); - }, - kibanaApplicationPrivilege(ApplicationPrivilege, kbnUrl, Promise, Private) { - return ApplicationPrivilege.query().$promise - .catch(checkLicenseError(kbnUrl, Promise, Private)); - }, - users(ShieldUser, kbnUrl, Promise, Private) { - // $promise is used here because the result is an ngResource, not a promise itself - return ShieldUser.query().$promise - .then(users => _.map(users, 'username')) - .catch(checkLicenseError(kbnUrl, Promise, Private)); - }, - indexPatterns(Private) { - const indexPatterns = Private(IndexPatternsProvider); - return indexPatterns.getTitles(); - } - }, - controllerAs: 'editRole', - controller($injector, $scope, rbacEnabled, rbacApplication) { - const $route = $injector.get('$route'); - const kbnUrl = $injector.get('kbnUrl'); - const shieldPrivileges = $injector.get('shieldPrivileges'); - const Notifier = $injector.get('Notifier'); - const Private = $injector.get('Private'); - const confirmModal = $injector.get('confirmModal'); - const shieldIndices = $injector.get('shieldIndices'); - - $scope.role = $route.current.locals.role; - $scope.users = $route.current.locals.users; - $scope.indexPatterns = $route.current.locals.indexPatterns; - $scope.privileges = shieldPrivileges; - - $scope.rbacEnabled = rbacEnabled; - const kibanaApplicationPrivilege = $route.current.locals.kibanaApplicationPrivilege; - const role = $route.current.locals.role; - $scope.kibanaPrivileges = getKibanaPrivileges(kibanaApplicationPrivilege, role, rbacApplication); - $scope.otherApplications = getOtherApplications(kibanaApplicationPrivilege, role, rbacApplication); - - $scope.rolesHref = `#${ROLES_PATH}`; - - this.isNewRole = $route.current.params.name == null; - this.fieldOptions = {}; - - const notifier = new Notifier(); - - $scope.deleteRole = (role) => { - const doDelete = () => { - role.$delete() - .then(() => toastNotifications.addSuccess('Deleted role')) - .then($scope.goToRoleList) - .catch(error => notifier.error(_.get(error, 'data.message'))); - }; - const confirmModalOptions = { - confirmButtonText: 'Delete role', - onConfirm: doDelete - }; - confirmModal('Are you sure you want to delete this role? This action is irreversible!', confirmModalOptions); - }; - - $scope.saveRole = (role) => { - role.indices = role.indices.filter((index) => index.names.length); - role.indices.forEach((index) => index.query || delete index.query); - - setApplicationPrivileges($scope.kibanaPrivileges, role, rbacApplication); - - return role.$save() - .then(() => toastNotifications.addSuccess('Updated role')) - .then($scope.goToRoleList) - .catch(error => notifier.error(_.get(error, 'data.message'))); - }; - - $scope.goToRoleList = () => { - kbnUrl.redirect(ROLES_PATH); - }; - - $scope.addIndex = indices => { - indices.push({ names: [], privileges: [], field_security: { grant: ['*'] } }); - }; - - $scope.areIndicesValid = (indices) => { - return indices - .filter((index) => index.names.length) - .find((index) => index.privileges.length === 0) == null; - }; - - $scope.fetchFieldOptions = (index) => { - const indices = index.names.join(','); - const fieldOptions = this.fieldOptions; - if (indices && fieldOptions[indices] == null) { - shieldIndices.getFields(indices) - .then((fields) => fieldOptions[indices] = fields) - .catch(() => fieldOptions[indices] = []); - } - }; - - $scope.isRoleEnabled = isRoleEnabled; - - const xpackInfo = Private(XPackInfoProvider); - $scope.allowDocumentLevelSecurity = xpackInfo.get('features.security.allowRoleDocumentLevelSecurity'); - $scope.allowFieldLevelSecurity = xpackInfo.get('features.security.allowRoleFieldLevelSecurity'); - - $scope.$watch('role.indices', (indices) => { - if (!indices.length) $scope.addIndex(indices); - else indices.forEach($scope.fetchFieldOptions); - }, true); - - $scope.toggle = toggle; - $scope.includes = _.includes; - $scope.togglePermission = (role, permission) => { - const shouldRemove = $scope.hasPermission(role, permission); - const rolePermissions = role.applications || []; - if (shouldRemove) { - role.applications = rolePermissions.filter(rolePermission => rolePermission.name === permission.name); - } else { - role.applications = rolePermissions.concat([permission]); - } - }; - - $scope.union = _.flow(_.union, _.compact); - } -}); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/__snapshots__/collapsible_panel.test.js.snap b/x-pack/plugins/security/public/views/management/edit_role/components/__snapshots__/collapsible_panel.test.js.snap new file mode 100644 index 0000000000000..d946357354fe2 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/__snapshots__/collapsible_panel.test.js.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it renders without blowing up 1`] = ` + + + + +

+ + + Elasticsearch +

+
+
+ + + hide + + +
+ + +

+ child +

+
+
+`; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/__snapshots__/page_header.test.js.snap b/x-pack/plugins/security/public/views/management/edit_role/components/__snapshots__/page_header.test.js.snap new file mode 100644 index 0000000000000..628a5c068f83a --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/__snapshots__/page_header.test.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it renders without crashing 1`] = ` +
+ + +
+`; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.js b/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.js new file mode 100644 index 0000000000000..f2bac7a02b99c --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.js @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import './collapsible_panel.less'; +import { + EuiPanel, + EuiLink, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; + +export class CollapsiblePanel extends Component { + static propTypes = { + iconType: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + } + + state = { + collapsed: false + } + + render() { + return ( + + {this.getTitle()} + {this.getForm()} + + ); + } + + getTitle = () => { + return ( + + + +

+ {this.props.title} +

+
+
+ + {this.state.collapsed ? 'show' : 'hide'} + +
+ ); + }; + + getForm = () => { + if (this.state.collapsed) { + return null; + } + + return ( + + + {this.props.children} + + ); + } + + toggleCollapsed = () => { + this.setState({ + collapsed: !this.state.collapsed + }); + } +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.less b/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.less new file mode 100644 index 0000000000000..ffb065880c560 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.less @@ -0,0 +1,4 @@ +.collapsiblePanel__logo { + margin-right: 8px; + vertical-align: text-bottom; +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.test.js b/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.test.js new file mode 100644 index 0000000000000..ec0182101653a --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel.test.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { CollapsiblePanel } from './collapsible_panel'; +import { EuiLink } from '@elastic/eui'; + +test('it renders without blowing up', () => { + const wrapper = shallow( + +

child

+
+ ); + + expect(wrapper).toMatchSnapshot(); +}); + +test('it renders children by default', () => { + const wrapper = mount( + +

child 1

+

child 2

+
+ ); + + expect(wrapper.find(CollapsiblePanel)).toHaveLength(1); + expect(wrapper.find('.child')).toHaveLength(2); +}); + +test('it hides children when the "hide" link is clicked', () => { + const wrapper = mount( + +

child 1

+

child 2

+
+ ); + + expect(wrapper.find(CollapsiblePanel)).toHaveLength(1); + expect(wrapper.find('.child')).toHaveLength(2); + + wrapper.find(EuiLink).simulate('click'); + + expect(wrapper.find('.child')).toHaveLength(0); +}); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.js b/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.js new file mode 100644 index 0000000000000..805dfb232fff4 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.js @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiButton, + EuiOverlayMask, + EuiConfirmModal, +} from '@elastic/eui'; + +export class DeleteRoleButton extends Component { + static propTypes = { + canDelete: PropTypes.bool.isRequired, + onDelete: PropTypes.func.isRequired + } + + state = { + showModal: false + } + + render() { + if (!this.props.canDelete) { + return null; + } + + return ( + + + Delete Role + + {this.maybeShowModal()} + + ); + } + + maybeShowModal = () => { + if (!this.state.showModal) { + return null; + } + return ( + + +

Are you sure you want to delete this role?

+

This action cannot be undone!

+
+
+ ); + } + + closeModal = () => { + this.setState({ + showModal: false + }); + } + + showModal = () => { + this.setState({ + showModal: true + }); + } + + onConfirmDelete = () => { + this.closeModal(); + this.props.onDelete(); + } +} + diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.test.js b/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.test.js new file mode 100644 index 0000000000000..477ffbebf8699 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/delete_role_button.test.js @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiButton, + EuiConfirmModal, +} from '@elastic/eui'; +import { DeleteRoleButton } from './delete_role_button'; +import { + shallow, + mount +} from 'enzyme'; + +test('it renders without crashing', () => { + const deleteHandler = jest.fn(); + const wrapper = shallow(); + expect(wrapper.find(EuiButton)).toHaveLength(1); + expect(deleteHandler).toHaveBeenCalledTimes(0); +}); + +test('it shows a confirmation dialog when clicked', () => { + const deleteHandler = jest.fn(); + const wrapper = mount(); + + wrapper.find(EuiButton).simulate('click'); + + expect(wrapper.find(EuiConfirmModal)).toHaveLength(1); + + expect(deleteHandler).toHaveBeenCalledTimes(0); +}); + +test('it renders nothing when canDelete is false', () => { + const deleteHandler = jest.fn(); + const wrapper = shallow(); + expect(wrapper.find('*')).toHaveLength(0); + expect(deleteHandler).toHaveBeenCalledTimes(0); +}); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.js b/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.js new file mode 100644 index 0000000000000..13cd7a4f1823b --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.js @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { get } from 'lodash'; +import { toastNotifications } from 'ui/notify'; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiPage, + EuiButtonEmpty, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import { PageHeader } from './page_header'; +import { saveRole, deleteRole } from '../../../../objects'; +import { isReservedRole } from '../../../../lib/role'; +import { RoleValidator } from '../lib/validate_role'; +import { ReservedRoleBadge } from './reserved_role_badge'; +import { ROLES_PATH } from '../../management_urls'; +import { DeleteRoleButton } from './delete_role_button'; +import { ElasticsearchPrivileges, KibanaPrivileges } from './privileges'; + +export class EditRolePage extends Component { + static propTypes = { + role: PropTypes.object.isRequired, + runAsUsers: PropTypes.array.isRequired, + indexPatterns: PropTypes.array.isRequired, + httpClient: PropTypes.func.isRequired, + rbacEnabled: PropTypes.bool.isRequired, + rbacApplication: PropTypes.string, + allowDocumentLevelSecurity: PropTypes.bool.isRequired, + allowFieldLevelSecurity: PropTypes.bool.isRequired, + kibanaAppPrivileges: PropTypes.array.isRequired, + notifier: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + role: props.role, + formError: null + }; + this.validator = new RoleValidator({ shouldValidate: false }); + } + + render() { + return ( + + + + {this.getFormTitle()} + + + + {this.getRoleName()} + + {this.getElasticsearchPrivileges()} + + {this.getKibanaPrivileges()} + + + + {this.getFormButtons()} + + + ); + } + + getFormTitle = () => { + let titleText; + if (isReservedRole(this.props.role)) { + titleText = 'Viewing role'; + } else if (this.editingExistingRole()) { + titleText = 'Edit role'; + } else { + titleText = 'Create role'; + } + + return ( +

{titleText}

+ ); + }; + + getActionButton = () => { + if (this.editingExistingRole() && !isReservedRole(this.props.role)) { + return ( + + + + ); + } + + return null; + }; + + getRoleName = () => { + return ( + + + + + + + ); + } + + onNameChange = (e) => { + const rawValue = e.target.value; + const name = rawValue.replace(/\s/g, '-'); + + this.setState({ + role: { + ...this.state.role, + name + } + }); + } + + getElasticsearchPrivileges() { + return ( +
+ + +
+ ); + } + + onRoleChange = (role) => { + this.setState({ + role + }); + } + + getKibanaPrivileges = () => { + if (!this.props.rbacEnabled) { + return null; + } + + return ( +
+ + +
+ ); + }; + + getFormButtons = () => { + if (isReservedRole(this.props.role)) { + return ( + + Return to role list + + ); + } + + const saveText = this.editingExistingRole() ? 'Update role' : 'Create role'; + + return ( + + + {saveText} + + + + Cancel + + + + {this.getActionButton()} + + ); + }; + + editingExistingRole = () => { + return !!this.props.role.name; + }; + + isPlaceholderPrivilege = (indexPrivilege) => { + return indexPrivilege.names.length === 0; + }; + + saveRole = () => { + this.validator.enableValidation(); + + const result = this.validator.validateForSave(this.state.role); + if (result.isInvalid) { + this.setState({ + formError: result + }); + } else { + this.setState({ + formError: null + }); + + const { + httpClient, + notifier, + } = this.props; + + const role = { + ...this.state.role + }; + + role.indices = role.indices.filter(i => !this.isPlaceholderPrivilege(i)); + role.indices.forEach((index) => index.query || delete index.query); + + saveRole(httpClient, role) + .then(() => { + toastNotifications.addSuccess('Saved role'); + this.backToRoleList(); + }) + .catch(error => { + notifier.error(get(error, 'data.message')); + }); + } + }; + + handleDeleteRole = () => { + const { + httpClient, + role, + notifier, + } = this.props; + + deleteRole(httpClient, role.name) + .then(() => { + toastNotifications.addSuccess('Deleted role'); + this.backToRoleList(); + }) + .catch(error => { + notifier.error(get(error, 'data.message')); + }); + }; + + backToRoleList = () => { + window.location.hash = ROLES_PATH; + }; +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/index.js b/x-pack/plugins/security/public/views/management/edit_role/components/index.js new file mode 100644 index 0000000000000..1a0afb37c4791 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EditRolePage } from './edit_role_page'; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/page_header.js b/x-pack/plugins/security/public/views/management/edit_role/components/page_header.js new file mode 100644 index 0000000000000..87aac82490817 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/page_header.js @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiBreadcrumbs, + EuiSpacer, +} from '@elastic/eui'; + +export class PageHeader extends Component { + render() { + return ( +
+ + +
+ ); + } + + buildBreadcrumb = (breadcrumb) => { + return { + text: breadcrumb.display, + href: breadcrumb.href, + }; + } +} + +PageHeader.propTypes = { + breadcrumbs: PropTypes.array.isRequired +}; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/page_header.test.js b/x-pack/plugins/security/public/views/management/edit_role/components/page_header.test.js new file mode 100644 index 0000000000000..99f6994ea9898 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/page_header.test.js @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { PageHeader } from './page_header'; +import { EuiBreadcrumbs } from '@elastic/eui'; + +test('it renders without crashing', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiBreadcrumbs)).toHaveLength(1); + expect(wrapper).toMatchSnapshot(); +}); + +test('it renders breadcrumbs', () => { + const breadcrumbs = [{ + display: 'item-1', + href: '#item-1', + }, { + display: 'item-2', + href: '#item-2', + }, { + display: 'item-3', + href: '#item-3', + }]; + + const wrapper = mount(); + expect(wrapper.find(EuiBreadcrumbs)).toHaveLength(1); +}); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/cluster_privileges.test.js.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/cluster_privileges.test.js.snap new file mode 100644 index 0000000000000..5315e277468ed --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/cluster_privileges.test.js.snap @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it renders without crashing 1`] = ` + + + + + + + + +`; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/elasticsearch_privileges.test.js.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/elasticsearch_privileges.test.js.snap new file mode 100644 index 0000000000000..dfc6b43dff636 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/elasticsearch_privileges.test.js.snap @@ -0,0 +1,160 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it renders without crashing 1`] = ` + + + + Manage the actions this role can perform against your cluster. + + Learn more + +

+ } + fullWidth={false} + gutterSize="l" + title={ +

+ Cluster privileges +

+ } + titleSize="xs" + > + + + +
+ + + Allow requests to be submitted on the behalf of other users. + + Learn more + +

+ } + fullWidth={false} + gutterSize="l" + title={ +

+ Run As privileges +

+ } + titleSize="xs" + > + + + +
+ + +

+ Index privileges +

+
+ + +

+ Control access to the data in your cluster. + + Learn more + +

+
+ + + + Add index privilege + +
+
+`; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/index_privilege_form.test.js.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/index_privilege_form.test.js.snap new file mode 100644 index 0000000000000..b84dcc659d5f9 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/index_privilege_form.test.js.snap @@ -0,0 +1,184 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it renders without crashing 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/index_privileges.test.js.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/index_privileges.test.js.snap new file mode 100644 index 0000000000000..189c4766c29f9 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/__snapshots__/index_privileges.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it renders without crashing 1`] = `Array []`; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/cluster_privileges.js b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/cluster_privileges.js new file mode 100644 index 0000000000000..e67ddd16a7fb3 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/cluster_privileges.js @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component } from 'react'; +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import { getClusterPrivileges } from '../../../../../services/role_privileges'; +import { isReservedRole } from '../../../../../lib/role'; +import { + EuiCheckboxGroup, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; + +export class ClusterPrivileges extends Component { + static propTypes = { + role: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, + }; + + render() { + + const clusterPrivileges = getClusterPrivileges(); + const privilegeGroups = _.chunk(clusterPrivileges, clusterPrivileges.length / 2); + + return ( + + {privilegeGroups.map(this.buildCheckboxGroup)} + + ); + } + + buildCheckboxGroup = (items, key) => { + const role = this.props.role; + + const checkboxes = items.map(i => ({ + id: i, + label: i + })); + + const selectionMap = (role.cluster || []) + .map(k => ({ [k]: true })) + .reduce((acc, o) => ({ ...acc, ...o }), {}); + + return ( + + + + ); + }; + + onClusterPrivilegesChange = (privilege) => { + const { cluster } = this.props.role; + const indexOfExistingPrivilege = cluster.indexOf(privilege); + + const shouldRemove = indexOfExistingPrivilege >= 0; + + const newClusterPrivs = [...cluster]; + if (shouldRemove) { + newClusterPrivs.splice(indexOfExistingPrivilege, 1); + } else { + newClusterPrivs.push(privilege); + } + + this.props.onChange(newClusterPrivs); + } +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/cluster_privileges.test.js b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/cluster_privileges.test.js new file mode 100644 index 0000000000000..f75cf0ade71a9 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/cluster_privileges.test.js @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { ClusterPrivileges } from './cluster_privileges'; +import { EuiCheckboxGroup } from '@elastic/eui'; + +test('it renders without crashing', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +test('it renders 2 checkbox groups of privileges', () => { + const wrapper = mount(); + expect(wrapper.find(EuiCheckboxGroup)).toHaveLength(2); +}); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/elasticsearch_privileges.js b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/elasticsearch_privileges.js new file mode 100644 index 0000000000000..a0c6eca6f0c6e --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/elasticsearch_privileges.js @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiText, + EuiSpacer, + EuiComboBox, + EuiFormRow, + EuiButton, + EuiDescribedFormGroup, + EuiTitle, + EuiHorizontalRule, + EuiLink, +} from '@elastic/eui'; +import './elasticsearch_privileges.less'; +import { ClusterPrivileges } from './cluster_privileges'; +import { IndexPrivileges } from './index_privileges'; +import { CollapsiblePanel } from '../collapsible_panel'; +import { documentationLinks } from '../../../../../documentation_links'; + +export class ElasticsearchPrivileges extends Component { + static propTypes = { + role: PropTypes.object.isRequired, + editable: PropTypes.bool.isRequired, + httpClient: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + runAsUsers: PropTypes.array.isRequired, + validator: PropTypes.object.isRequired, + indexPatterns: PropTypes.array.isRequired, + allowDocumentLevelSecurity: PropTypes.bool.isRequired, + allowFieldLevelSecurity: PropTypes.bool.isRequired, + }; + + render() { + return ( + + {this.getForm()} + + ); + } + + getForm = () => { + const { + role, + httpClient, + validator, + onChange, + indexPatterns, + allowDocumentLevelSecurity, + allowFieldLevelSecurity, + } = this.props; + + const indexProps = { + role, + httpClient, + validator, + indexPatterns, + allowDocumentLevelSecurity, + allowFieldLevelSecurity, + onChange, + }; + + return ( + + Cluster privileges} + description={ +

+ Manage the actions this role can perform against your cluster. {this.learnMore(documentationLinks.esClusterPrivileges)} +

+ } + > + + + +
+ + + + Run As privileges} + description={ +

+ Allow requests to be submitted on the behalf of other users. {this.learnMore(documentationLinks.esRunAsPrivileges)} +

+ } + > + + ({ id: username, label: username }))} + selectedOptions={this.props.role.run_as.map(u => ({ label: u }))} + onChange={this.onRunAsUserChange} + isDisabled={!this.props.editable} + /> + +
+ + + +

Index privileges

+ + +

Control access to the data in your cluster. {this.learnMore(documentationLinks.esIndicesPrivileges)}

+
+ + + + + + {this.props.editable && ( + Add index privilege + )} +
+ ); + } + + learnMore = (href) => ( + + Learn more + + ); + + addIndexPrivilege = () => { + const { role } = this.props; + + const newIndices = [...role.indices, { + names: [], + privileges: [], + field_security: { + grant: ['*'] + } + }]; + + this.props.onChange({ + ...this.props.role, + indices: newIndices + }); + }; + + onClusterPrivilegesChange = (cluster) => { + const role = { + ...this.props.role, + cluster + }; + + this.props.onChange(role); + } + + onRunAsUserChange = (users) => { + const role = { + ...this.props.role, + run_as: users.map(u => u.label) + }; + + this.props.onChange(role); + } +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/elasticsearch_privileges.less b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/elasticsearch_privileges.less new file mode 100644 index 0000000000000..776ef72a4627e --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/elasticsearch_privileges.less @@ -0,0 +1,3 @@ +.editRole__learnMore { + margin-left: 5px; +} \ No newline at end of file diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/elasticsearch_privileges.test.js b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/elasticsearch_privileges.test.js new file mode 100644 index 0000000000000..7a2af5723e543 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/elasticsearch_privileges.test.js @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { IndexPrivileges } from './index_privileges'; +import { ClusterPrivileges } from './cluster_privileges'; +import { ElasticsearchPrivileges } from './elasticsearch_privileges'; +import { RoleValidator } from '../../lib/validate_role'; + +test('it renders without crashing', () => { + const props = { + role: { + cluster: [], + indices: [], + run_as: [] + }, + editable: true, + httpClient: jest.fn(), + onChange: jest.fn(), + runAsUsers: [], + indexPatterns: [], + allowDocumentLevelSecurity: true, + allowFieldLevelSecurity: true, + validator: new RoleValidator(), + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +test('it renders ClusterPrivileges', () => { + const props = { + role: { + cluster: [], + indices: [], + run_as: [] + }, + editable: true, + httpClient: jest.fn(), + onChange: jest.fn(), + runAsUsers: [], + indexPatterns: [], + allowDocumentLevelSecurity: true, + allowFieldLevelSecurity: true, + validator: new RoleValidator(), + }; + const wrapper = mount(); + expect(wrapper.find(ClusterPrivileges)).toHaveLength(1); +}); + +test('it renders IndexPrivileges', () => { + const props = { + role: { + cluster: [], + indices: [], + run_as: [] + }, + editable: true, + httpClient: jest.fn(), + onChange: jest.fn(), + runAsUsers: [], + indexPatterns: [], + allowDocumentLevelSecurity: true, + allowFieldLevelSecurity: true, + validator: new RoleValidator(), + }; + const wrapper = mount(); + expect(wrapper.find(IndexPrivileges)).toHaveLength(1); +}); \ No newline at end of file diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index.js b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index.js new file mode 100644 index 0000000000000..d142af394b155 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ElasticsearchPrivileges } from './elasticsearch_privileges'; +export { KibanaPrivileges } from './kibana_privileges'; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privilege_form.js b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privilege_form.js new file mode 100644 index 0000000000000..ee62b80c5e282 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privilege_form.js @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiComboBox, + EuiTextArea, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiSpacer, + EuiHorizontalRule, + EuiButtonIcon, +} from '@elastic/eui'; +import { getIndexPrivileges } from '../../../../../services/role_privileges'; + +const fromOption = (option) => option.label; +const toOption = (value) => ({ label: value }); + +export class IndexPrivilegeForm extends Component { + static propTypes = { + indexPrivilege: PropTypes.object.isRequired, + indexPatterns: PropTypes.array.isRequired, + availableFields: PropTypes.array, + onChange: PropTypes.func.isRequired, + isReservedRole: PropTypes.bool.isRequired, + allowDelete: PropTypes.bool.isRequired, + allowDocumentLevelSecurity: PropTypes.bool.isRequired, + allowFieldLevelSecurity: PropTypes.bool.isRequired, + validator: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + queryExpanded: !!props.indexPrivilege.query, + documentQuery: props.indexPrivilege.query + }; + } + + render() { + return ( + + + + + {this.getPrivilegeForm()} + + {this.props.allowDelete && ( + + + + + + )} + + + ); + } + + getPrivilegeForm = () => { + return ( + + + + + + + + + + + + + {this.getGrantedFieldsControl()} + + + + + {this.getGrantedDocumentsControl()} + + ); + }; + + getGrantedFieldsControl = () => { + const { + allowFieldLevelSecurity, + availableFields, + indexPrivilege, + isReservedRole, + } = this.props; + + if (!allowFieldLevelSecurity) { + return null; + } + + const { grant = [] } = indexPrivilege.field_security || {}; + + if (allowFieldLevelSecurity) { + return ( + + + + + + + + ); + } + + return null; + } + + getGrantedDocumentsControl = () => { + const { + allowDocumentLevelSecurity, + indexPrivilege, + } = this.props; + + if (!allowDocumentLevelSecurity) { + return null; + } + + return ( + + {!this.props.isReservedRole && + + + + } + {this.state.queryExpanded && + + + + + + } + + ); + }; + + toggleDocumentQuery = () => { + const willToggleOff = this.state.queryExanded; + const willToggleOn = !willToggleOff; + + // If turning off, then save the current query in state so that we can restore it if the user changes their mind. + this.setState({ + queryExpanded: !this.state.queryExpanded, + documentQuery: willToggleOff ? this.props.indexPrivilege.query : this.state.documentQuery + }); + + // If turning off, then remove the query from the Index Privilege + if (willToggleOff) { + this.props.onChange({ + ...this.props.indexPrivilege, + query: '', + }); + } + + // If turning on, then restore the saved query if available + if (willToggleOn && !this.props.indexPrivilege.query && this.state.documentQuery) { + this.props.onChange({ + ...this.props.indexPrivilege, + query: this.state.documentQuery, + }); + } + }; + + onCreateIndexPatternOption = (option) => { + const newIndexPatterns = this.props.indexPrivilege.names.concat([option]); + + this.props.onChange({ + ...this.props.indexPrivilege, + names: newIndexPatterns, + }); + }; + + onIndexPatternsChange = (newPatterns) => { + this.props.onChange({ + ...this.props.indexPrivilege, + names: newPatterns.map(fromOption), + }); + }; + + onPrivilegeChange = (newPrivileges) => { + this.props.onChange({ + ...this.props.indexPrivilege, + privileges: newPrivileges.map(fromOption), + }); + }; + + onQueryChange = (e) => { + this.props.onChange({ + ...this.props.indexPrivilege, + query: e.target.value, + }); + }; + + onCreateGrantedField = (grant) => { + const newGrants = this.props.indexPrivilege.field_security.grant.concat([grant]); + + this.props.onChange({ + ...this.props.indexPrivilege, + field_security: { + ...this.props.indexPrivilege.field_security, + grant: newGrants, + }, + }); + }; + + onGrantedFieldsChange = (grantedFields) => { + this.props.onChange({ + ...this.props.indexPrivilege, + field_security: { + ...this.props.indexPrivilege.field_security, + grant: grantedFields.map(fromOption), + }, + }); + }; +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privilege_form.test.js b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privilege_form.test.js new file mode 100644 index 0000000000000..1fed539d2526d --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privilege_form.test.js @@ -0,0 +1,206 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { IndexPrivilegeForm } from './index_privilege_form'; +import { RoleValidator } from '../../lib/validate_role'; +import { EuiSwitch, EuiTextArea, EuiButtonIcon } from '@elastic/eui'; + +test('it renders without crashing', () => { + const props = { + indexPrivilege: { + names: [], + privileges: [], + query: null, + field_security: { + grant: [] + } + }, + indexPatterns: [], + availableFields: [], + isReservedRole: false, + allowDelete: true, + allowDocumentLevelSecurity: true, + allowFieldLevelSecurity: true, + validator: new RoleValidator(), + onChange: jest.fn() + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +describe('delete button', () => { + const props = { + indexPrivilege: { + names: [], + privileges: [], + query: null, + field_security: { + grant: [] + } + }, + indexPatterns: [], + availableFields: [], + isReservedRole: false, + allowDelete: true, + allowDocumentLevelSecurity: true, + allowFieldLevelSecurity: true, + validator: new RoleValidator(), + onChange: jest.fn(), + onDelete: jest.fn() + }; + + test('it is hidden when allowDelete is false', () => { + const testProps = { + ...props, + allowDelete: false + }; + const wrapper = mount(); + expect(wrapper.find(EuiButtonIcon)).toHaveLength(0); + }); + + test('it is shown when allowDelete is true', () => { + const testProps = { + ...props, + allowDelete: true + }; + const wrapper = mount(); + expect(wrapper.find(EuiButtonIcon)).toHaveLength(1); + }); + + test('it invokes onDelete when clicked', () => { + const testProps = { + ...props, + allowDelete: true + }; + const wrapper = mount(); + wrapper.find(EuiButtonIcon).simulate('click'); + expect(testProps.onDelete).toHaveBeenCalledTimes(1); + }); +}); + +describe(`document level security`, () => { + const props = { + indexPrivilege: { + names: [], + privileges: [], + query: "some query", + field_security: { + grant: [] + } + }, + indexPatterns: [], + availableFields: [], + isReservedRole: false, + allowDelete: true, + allowDocumentLevelSecurity: true, + allowFieldLevelSecurity: true, + validator: new RoleValidator(), + onChange: jest.fn() + }; + + test(`inputs are hidden when DLS is not allowed`, () => { + const testProps = { + ...props, + allowDocumentLevelSecurity: false + }; + + const wrapper = mount(); + expect(wrapper.find(EuiSwitch)).toHaveLength(0); + expect(wrapper.find(EuiTextArea)).toHaveLength(0); + }); + + test('only the switch is shown when allowed, and query is empty', () => { + const testProps = { + ...props, + indexPrivilege: { + ...props.indexPrivilege, + query: null + } + }; + + const wrapper = mount(); + expect(wrapper.find(EuiSwitch)).toHaveLength(1); + expect(wrapper.find(EuiTextArea)).toHaveLength(0); + }); + + test('both inputs are shown when allowed, and query is not empty', () => { + const testProps = { + ...props, + }; + + const wrapper = mount(); + expect(wrapper.find(EuiSwitch)).toHaveLength(1); + expect(wrapper.find(EuiTextArea)).toHaveLength(1); + }); +}); + +describe('field level security', () => { + const props = { + indexPrivilege: { + names: [], + privileges: [], + query: null, + field_security: { + grant: ["foo*"] + } + }, + indexPatterns: [], + availableFields: [], + isReservedRole: false, + allowDelete: true, + allowDocumentLevelSecurity: true, + allowFieldLevelSecurity: true, + validator: new RoleValidator(), + onChange: jest.fn() + }; + + test(`input is hidden when FLS is not allowed`, () => { + const testProps = { + ...props, + allowFieldLevelSecurity: false + }; + + const wrapper = mount(); + expect(wrapper.find(".indexPrivilegeForm__grantedFieldsRow")).toHaveLength(0); + }); + + test('input is shown when allowed', () => { + const testProps = { + ...props, + }; + + const wrapper = mount(); + expect(wrapper.find("div.indexPrivilegeForm__grantedFieldsRow")).toHaveLength(1); + }); + + test('it displays a warning when no fields are granted', () => { + const testProps = { + ...props, + indexPrivilege: { + ...props.indexPrivilege, + field_security: { + grant: [] + } + } + }; + + const wrapper = mount(); + expect(wrapper.find("div.indexPrivilegeForm__grantedFieldsRow")).toHaveLength(1); + expect(wrapper.find(".euiFormHelpText")).toHaveLength(1); + }); + + test('it does not display a warning when fields are granted', () => { + const testProps = { + ...props + }; + + const wrapper = mount(); + expect(wrapper.find("div.indexPrivilegeForm__grantedFieldsRow")).toHaveLength(1); + expect(wrapper.find(".euiFormHelpText")).toHaveLength(0); + }); +}); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privileges.js b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privileges.js new file mode 100644 index 0000000000000..cb72ecd7aa4bf --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privileges.js @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import { isReservedRole, isRoleEnabled } from '../../../../../lib/role'; +import { IndexPrivilegeForm } from './index_privilege_form'; +import { getFields } from '../../../../../objects'; + +export class IndexPrivileges extends Component { + static propTypes = { + role: PropTypes.object.isRequired, + indexPatterns: PropTypes.array.isRequired, + allowDocumentLevelSecurity: PropTypes.bool.isRequired, + allowFieldLevelSecurity: PropTypes.bool.isRequired, + httpClient: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + validator: PropTypes.object.isRequired, + } + + state = { + availableFields: {} + } + + componentDidMount() { + this.loadAvailableFields(this.props.role.indices); + } + + render() { + const { indices = [] } = this.props.role; + + const { + indexPatterns, + allowDocumentLevelSecurity, + allowFieldLevelSecurity + } = this.props; + + const props = { + indexPatterns, + // If editing an existing role while that has been disabled, always show the FLS/DLS fields because currently + // a role is only marked as disabled if it has FLS/DLS setup (usually before the user changed to a license that + // doesn't permit FLS/DLS). + allowDocumentLevelSecurity: allowDocumentLevelSecurity || !isRoleEnabled(this.props.role), + allowFieldLevelSecurity: allowFieldLevelSecurity || !isRoleEnabled(this.props.role), + isReservedRole: isReservedRole(this.props.role) + }; + + const forms = indices.map((indexPrivilege, idx) => ( + + )); + + return forms; + } + + addIndexPrivilege = () => { + const { role } = this.props; + + const newIndices = [...role.indices, { + names: [], + privileges: [], + field_security: { + grant: ['*'] + } + }]; + + this.props.onChange({ + ...this.props.role, + indices: newIndices + }); + }; + + onIndexPrivilegeChange = (privilegeIndex) => { + return (updatedPrivilege) => { + const { role } = this.props; + const { indices } = role; + + const newIndices = [...indices]; + newIndices[privilegeIndex] = updatedPrivilege; + + this.props.onChange({ + ...this.props.role, + indices: newIndices + }); + + this.loadAvailableFields(newIndices); + }; + }; + + onIndexPrivilegeDelete = (privilegeIndex) => { + return () => { + const { role } = this.props; + + const newIndices = [...role.indices]; + newIndices.splice(privilegeIndex, 1); + + this.props.onChange({ + ...this.props.role, + indices: newIndices + }); + }; + } + + isPlaceholderPrivilege = (indexPrivilege) => { + return indexPrivilege.names.length === 0; + }; + + loadAvailableFields(indices) { + // Reserved roles cannot be edited, and therefore do not need to fetch available fields. + if (isReservedRole(this.props.role)) { + return; + } + + const patterns = indices.map(index => index.names.join(',')); + + const cachedPatterns = Object.keys(this.state.availableFields); + const patternsToFetch = _.difference(patterns, cachedPatterns); + + const fetchRequests = patternsToFetch.map(this.loadFieldsForPattern); + + Promise.all(fetchRequests) + .then(response => { + + this.setState({ + availableFields: { + ...this.state.availableFields, + ...response.reduce((acc, o) => ({ ...acc, ...o }), {}) + } + }); + }); + } + + loadFieldsForPattern = async (pattern) => { + if (!pattern) return { [pattern]: [] }; + + try { + return { + [pattern]: await getFields(this.props.httpClient, pattern) + }; + + } catch (e) { + return { + [pattern]: [] + }; + } + } +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privileges.test.js b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privileges.test.js new file mode 100644 index 0000000000000..221d301ceef0f --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/index_privileges.test.js @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { IndexPrivileges } from './index_privileges'; +import { IndexPrivilegeForm } from './index_privilege_form'; +import { RoleValidator } from '../../lib/validate_role'; + +test('it renders without crashing', () => { + const props = { + role: { + cluster: [], + indices: [], + run_as: [] + }, + httpClient: jest.fn(), + onChange: jest.fn(), + indexPatterns: [], + allowDocumentLevelSecurity: true, + allowFieldLevelSecurity: true, + validator: new RoleValidator(), + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +test('it renders a IndexPrivilegeForm for each privilege on the role', () => { + const props = { + role: { + cluster: [], + indices: [{ + names: ['foo*'], + privileges: ['all'], + query: '*', + field_security: { + grant: ['some_field'] + } + }], + run_as: [] + }, + httpClient: jest.fn(), + onChange: jest.fn(), + indexPatterns: [], + allowDocumentLevelSecurity: true, + allowFieldLevelSecurity: true, + validator: new RoleValidator(), + }; + const wrapper = mount(); + expect(wrapper.find(IndexPrivilegeForm)).toHaveLength(1); +}); \ No newline at end of file diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana_privileges.js b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana_privileges.js new file mode 100644 index 0000000000000..a8c2a2c52cbe0 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana_privileges.js @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { isReservedRole } from '../../../../../lib/role'; +import { getKibanaPrivileges } from '../../lib/get_application_privileges'; +import { setApplicationPrivileges } from '../../lib/set_application_privileges'; + +import { CollapsiblePanel } from '../collapsible_panel'; +import { + EuiSelect, + EuiDescribedFormGroup, + EuiFormRow, +} from '@elastic/eui'; + +const noPrivilegeValue = '-none-'; + +export class KibanaPrivileges extends Component { + static propTypes = { + role: PropTypes.object.isRequired, + spaces: PropTypes.array, + kibanaAppPrivileges: PropTypes.array.isRequired, + onChange: PropTypes.func.isRequired, + }; + + idPrefix = () => `${this.props.rbacApplication}_`; + + privilegeToId = (privilege) => `${this.idPrefix()}${privilege}`; + + idToPrivilege = (id) => id.split(this.idPrefix())[1]; + + render() { + return ( + + {this.getForm()} + + ); + } + + getForm = () => { + const { + kibanaAppPrivileges, + role, + rbacApplication + } = this.props; + + const kibanaPrivileges = getKibanaPrivileges(kibanaAppPrivileges, role, rbacApplication); + + const options = [ + { value: noPrivilegeValue, text: 'none' }, + ...Object.keys(kibanaPrivileges).map(p => ({ + value: p, + text: p + })) + ]; + + const value = Object.keys(kibanaPrivileges).find(p => kibanaPrivileges[p]) || noPrivilegeValue; + + return ( + Application privileges

} + description={

Manage the actions this role can perform within Kibana.

} + > + + + +
+ ); + } + + onKibanaPrivilegesChange = (e) => { + const role = { + ...this.props.role, + applications: [...this.props.role.applications] + }; + + const privilege = e.target.value; + + if (privilege === noPrivilegeValue) { + // unsetting all privileges -- only necessary until RBAC Phase 3 + const noPrivileges = {}; + setApplicationPrivileges(noPrivileges, role, this.props.rbacApplication); + } else { + const newPrivileges = { + [privilege]: true + }; + setApplicationPrivileges(newPrivileges, role, this.props.rbacApplication); + } + + this.props.onChange(role); + } +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/reserved_role_badge.js b/x-pack/plugins/security/public/views/management/edit_role/components/reserved_role_badge.js new file mode 100644 index 0000000000000..8b750b19338ed --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/reserved_role_badge.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { isReservedRole } from '../../../../lib/role'; +import { + EuiIcon, + EuiToolTip, +} from '@elastic/eui'; + + +export const ReservedRoleBadge = (props) => { + const { + role + } = props; + + if (isReservedRole(role)) { + return ( + + + + ); + } + return null; +}; + +ReservedRoleBadge.propTypes = { + role: PropTypes.object.isRequired +}; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.js b/x-pack/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.js new file mode 100644 index 0000000000000..939348661a741 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.js @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiIcon +} from '@elastic/eui'; +import { ReservedRoleBadge } from './reserved_role_badge'; +import { + shallow +} from 'enzyme'; + +const reservedRole = { + metadata: { + _reserved: true + } +}; + +const unreservedRole = {}; + +test('it renders without crashing', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiIcon)).toHaveLength(1); +}); + +test('it renders nothing for an unreserved role', () => { + const wrapper = shallow(); + expect(wrapper.find('*')).toHaveLength(0); +}); diff --git a/x-pack/plugins/security/public/views/management/edit_role/edit_role.html b/x-pack/plugins/security/public/views/management/edit_role/edit_role.html new file mode 100644 index 0000000000000..5539f0521dd79 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/edit_role.html @@ -0,0 +1 @@ +
diff --git a/x-pack/plugins/security/public/views/management/edit_role/edit_role.less b/x-pack/plugins/security/public/views/management/edit_role/edit_role.less new file mode 100644 index 0000000000000..d4f7ac04880d6 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/edit_role.less @@ -0,0 +1,10 @@ +#editRoleReactRoot { + background: #f5f5f5; + flex-grow: 1; +} + +.editRolePage { + max-width: 1000px; + margin-left: auto; + margin-right: auto; +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/index.js b/x-pack/plugins/security/public/views/management/edit_role/index.js new file mode 100644 index 0000000000000..e356b9d7c01b6 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/index.js @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import routes from 'ui/routes'; +import { fatalError } from 'ui/notify'; +import template from 'plugins/security/views/management/edit_role/edit_role.html'; +import 'plugins/security/views/management/edit_role/edit_role.less'; +import 'angular-ui-select'; +import 'plugins/security/services/application_privilege'; +import 'plugins/security/services/shield_user'; +import 'plugins/security/services/shield_role'; +import 'plugins/security/services/shield_privileges'; +import 'plugins/security/services/shield_indices'; + +import { IndexPatternsProvider } from 'ui/index_patterns/index_patterns'; +import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; +import { checkLicenseError } from 'plugins/security/lib/check_license_error'; +import { EDIT_ROLES_PATH, ROLES_PATH } from '../management_urls'; + +import { EditRolePage } from './components'; + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +routes.when(`${EDIT_ROLES_PATH}/:name?`, { + template, + resolve: { + role($route, ShieldRole, kbnUrl, Promise, Notifier) { + const name = $route.current.params.name; + + let role; + + if (name != null) { + role = ShieldRole.get({ name }).$promise + .catch((response) => { + + if (response.status !== 404) { + return fatalError(response); + } + + const notifier = new Notifier(); + notifier.error(`No "${name}" role found.`); + kbnUrl.redirect(ROLES_PATH); + return Promise.halt(); + }); + + } else { + role = Promise.resolve(new ShieldRole({ + cluster: [], + indices: [], + run_as: [], + applications: [] + })); + } + + return role.then(res => res.toJSON()); + }, + kibanaApplicationPrivilege(ApplicationPrivilege, kbnUrl, Promise, Private) { + return ApplicationPrivilege.query().$promise + .then(privileges => privileges.map(p => p.toJSON())) + .catch(checkLicenseError(kbnUrl, Promise, Private)); + }, + users(ShieldUser, kbnUrl, Promise, Private) { + // $promise is used here because the result is an ngResource, not a promise itself + return ShieldUser.query().$promise + .then(users => _.map(users, 'username')) + .catch(checkLicenseError(kbnUrl, Promise, Private)); + }, + indexPatterns(Private) { + const indexPatterns = Private(IndexPatternsProvider); + return indexPatterns.getTitles(); + } + }, + controllerAs: 'editRole', + controller($injector, $scope, $http, rbacEnabled, rbacApplication) { + const $route = $injector.get('$route'); + const Private = $injector.get('Private'); + + const Notifier = $injector.get('Notifier'); + + const kibanaApplicationPrivilege = $route.current.locals.kibanaApplicationPrivilege; + const role = $route.current.locals.role; + + const xpackInfo = Private(XPackInfoProvider); + const allowDocumentLevelSecurity = xpackInfo.get('features.security.allowRoleDocumentLevelSecurity'); + const allowFieldLevelSecurity = xpackInfo.get('features.security.allowRoleFieldLevelSecurity'); + + const domNode = document.getElementById('editRoleReactRoot'); + + const { + users, + indexPatterns, + } = $route.current.locals; + + const routeBreadcrumbs = routes.getBreadcrumbs(); + + render(, domNode); + + // unmount react on controller destroy + $scope.$on('$destroy', () => { + unmountComponentAtNode(domNode); + }); + } +}); + +function transformBreadcrumbs(routeBreadcrumbs) { + const indexOfEdit = routeBreadcrumbs.findIndex(b => b.id === 'edit'); + + const hasEntryAfterEdit = indexOfEdit >= 0 && indexOfEdit < (routeBreadcrumbs.length - 1); + + if (hasEntryAfterEdit) { + // The entry after 'edit' is the name of the role being edited (if any). We don't want to use the "humanized" version of the role name here + const roleName = routeBreadcrumbs[indexOfEdit + 1]; + roleName.display = roleName.id; + } + + return routeBreadcrumbs.filter(b => b.id !== 'edit'); +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.js.snap b/x-pack/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.js.snap new file mode 100644 index 0000000000000..20b4280b7f493 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validateIndexPrivileges it throws when indices is not an array 1`] = `"Expected role.indices to be an array"`; diff --git a/x-pack/plugins/security/public/views/management/edit_role/lib/get_application_privileges.js b/x-pack/plugins/security/public/views/management/edit_role/lib/get_application_privileges.js new file mode 100644 index 0000000000000..1ee8d201806b6 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/lib/get_application_privileges.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import _ from 'lodash'; + +export function getKibanaPrivileges(kibanaApplicationPrivilege, role, application) { + const kibanaPrivileges = kibanaApplicationPrivilege.reduce((acc, p) => { + acc[p.name] = false; + return acc; + }, {}); + + if (!role.applications || role.applications.length === 0) { + return kibanaPrivileges; + } + + const applications = role.applications.filter(x => x.application === application); + + const assigned = _.uniq(_.flatten(_.pluck(applications, 'privileges'))); + assigned.forEach(a => { + kibanaPrivileges[a] = true; + }); + + return kibanaPrivileges; +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/lib/set_application_privileges.js b/x-pack/plugins/security/public/views/management/edit_role/lib/set_application_privileges.js new file mode 100644 index 0000000000000..e6e133fbf69bd --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/lib/set_application_privileges.js @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DEFAULT_RESOURCE } from '../../../../../common/constants'; + +export function setApplicationPrivileges(kibanaPrivileges, role, application) { + if (!role.applications) { + role.applications = []; + } + + // we first remove the matching application entries + role.applications = role.applications.filter(x => { + return x.application !== application; + }); + + const privileges = Object.keys(kibanaPrivileges).filter(key => kibanaPrivileges[key]); + + // if we still have them, put the application entry back + if (privileges.length > 0) { + role.applications = [...role.applications, { + application, + privileges, + resources: [DEFAULT_RESOURCE] + }]; + } +} + +export function togglePrivilege(role, application, permission) { + const appPermissions = role.applications + .find(a => a.application === application && a.resources[0] === DEFAULT_RESOURCE); + + if (!appPermissions) { + role.applications.push({ + application, + privileges: [permission], + resources: [DEFAULT_RESOURCE] + }); + } else { + const indexOfExisting = appPermissions.privileges.indexOf(permission); + if (indexOfExisting >= 0) { + appPermissions.privileges.splice(indexOfExisting, 1); + } else { + appPermissions.privileges.push(permission); + } + } +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/lib/validate_role.js b/x-pack/plugins/security/public/views/management/edit_role/lib/validate_role.js new file mode 100644 index 0000000000000..3fc638a83782c --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/lib/validate_role.js @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class RoleValidator { + constructor(options = {}) { + this._shouldValidate = options.shouldValidate; + } + + enableValidation() { + this._shouldValidate = true; + } + + disableValidation() { + this._shouldValidate = false; + } + + validateRoleName(role) { + if (!this._shouldValidate) return valid(); + + if (!role.name) { + return invalid(`Please provide a role name`); + } + if (role.name.length > 1024) { + return invalid(`Name must not exceed 1024 characters`); + } + if (!role.name.match(/^[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*$/)) { + return invalid(`Name must begin with a letter or underscore and contain only letters, underscores, and numbers.`); + } + return valid(); + } + + validateIndexPrivileges(role) { + if (!this._shouldValidate) return valid(); + + if (!Array.isArray(role.indices)) { + throw new TypeError(`Expected role.indices to be an array`); + } + + const areIndicesValid = role.indices + .map(this.validateIndexPrivilege.bind(this)) + .find((result) => result.isInvalid) == null; + + if (areIndicesValid) { + return valid(); + } + return invalid(); + } + + validateIndexPrivilege(indexPrivilege) { + if (!this._shouldValidate) return valid(); + + if (indexPrivilege.names.length && !indexPrivilege.privileges.length) { + return invalid(`At least one privilege is required`); + } + return valid(); + } + + validateForSave(role) { + const { isInvalid: isNameInvalid } = this.validateRoleName(role); + const { isInvalid: areIndicesInvalid } = this.validateIndexPrivileges(role); + + if (isNameInvalid || areIndicesInvalid) { + return invalid(); + } + + return valid(); + } + +} + +function invalid(error) { + return { + isInvalid: true, + error + }; +} + +function valid() { + return { + isInvalid: false + }; +} + diff --git a/x-pack/plugins/security/public/views/management/edit_role/lib/validate_role.test.js b/x-pack/plugins/security/public/views/management/edit_role/lib/validate_role.test.js new file mode 100644 index 0000000000000..47d2bfe59a37e --- /dev/null +++ b/x-pack/plugins/security/public/views/management/edit_role/lib/validate_role.test.js @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { RoleValidator } from "./validate_role"; + +let validator; + +describe('validateRoleName', () => { + beforeEach(() => { + validator = new RoleValidator({ shouldValidate: true }); + }); + + test('it allows an alphanumeric role name', () => { + const role = { + name: 'This-is-30-character-role-name' + }; + + expect(validator.validateRoleName(role)).toEqual({ isInvalid: false }); + }); + + test('it requires a non-empty value', () => { + const role = { + name: '' + }; + + expect(validator.validateRoleName(role)).toEqual({ isInvalid: true, error: `Please provide a role name` }); + }); + + test('it cannot exceed 1024 characters', () => { + const role = { + name: new Array(1026).join('A') + }; + + expect(validator.validateRoleName(role)).toEqual({ isInvalid: true, error: `Name must not exceed 1024 characters` }); + }); + + const charList = `!#%^&*()+=[]{}\|';:"/,<>?`.split(''); + charList.forEach(element => { + test(`it cannot support the "${element}" character`, () => { + const role = { + name: `role-${element}` + }; + + expect(validator.validateRoleName(role)).toEqual( + { + isInvalid: true, + error: `Name must begin with a letter or underscore and contain only letters, underscores, and numbers.` + } + ); + }); + }); +}); + +describe('validateIndexPrivileges', () => { + beforeEach(() => { + validator = new RoleValidator({ shouldValidate: true }); + }); + + test('it ignores privilegs with no indices defined', () => { + const role = { + indices: [{ + names: [], + privileges: [] + }] + }; + + expect(validator.validateIndexPrivileges(role)).toEqual({ + isInvalid: false + }); + }); + + test('it requires privilges when an index is defined', () => { + const role = { + indices: [{ + names: ['index-*'], + privileges: [] + }] + }; + + expect(validator.validateIndexPrivileges(role)).toEqual({ + isInvalid: true + }); + }); + + test('it throws when indices is not an array', () => { + const role = { + indices: null + }; + + expect(() => validator.validateIndexPrivileges(role)).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/x-pack/plugins/security/public/views/management/index_privileges_form/index_privileges_form.html b/x-pack/plugins/security/public/views/management/index_privileges_form/index_privileges_form.html deleted file mode 100644 index 829b0dfaefa92..0000000000000 --- a/x-pack/plugins/security/public/views/management/index_privileges_form/index_privileges_form.html +++ /dev/null @@ -1,128 +0,0 @@ -
- - - -
-
- -
- -
- - - {{$item}} - -
-
-
- - -
- - - - {{$item}} - -
-
-
- - -
- Indices must contain at least one privilege. -
- - -
- - - -
-
- - -
- -
- - - - -
- - - - {{$item}} - -
-
-
- -
-
- - - -
-
-
-
-
-
diff --git a/x-pack/plugins/security/public/views/management/index_privileges_form/index_privileges_form.js b/x-pack/plugins/security/public/views/management/index_privileges_form/index_privileges_form.js deleted file mode 100644 index 1bb058f56d096..0000000000000 --- a/x-pack/plugins/security/public/views/management/index_privileges_form/index_privileges_form.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import { uiModules } from 'ui/modules'; -import template from './index_privileges_form.html'; - -const module = uiModules.get('security', ['kibana']); -module.directive('kbnIndexPrivilegesForm', function () { - return { - template, - scope: { - isNewRole: '=', - indices: '=', - indexPatterns: '=', - privileges: '=', - fieldOptions: '=', - isReserved: '=', - isEnabled: '=', - allowDocumentLevelSecurity: '=', - allowFieldLevelSecurity: '=', - addIndex: '&', - removeIndex: '&', - }, - restrict: 'E', - replace: true, - controllerAs: 'indexPrivilegesController', - controller: function ($scope) { - this.addIndex = function addIndex() { - $scope.addIndex({ indices: $scope.indices }); - }; - - this.removeIndex = function removeIndex(index) { - $scope.removeIndex({ indices: $scope.indices, index }); - }; - - this.getIndexTitle = function getIndexTitle(index) { - const indices = index.names.length ? index.names.join(', ') : 'No indices'; - const privileges = index.privileges.length ? index.privileges.join(', ') : 'No privileges'; - return `${indices} (${privileges})`; - }; - - this.union = _.flow(_.union, _.compact); - - // If editing an existing role while that has been disabled, always show the FLS/DLS fields because currently - // a role is only marked as disabled if it has FLS/DLS setup (usually before the user changed to a license that - // doesn't permit FLS/DLS). - if (!$scope.isNewRole && !$scope.isEnabled) { - this.showDocumentLevelSecurity = true; - this.showFieldLevelSecurity = true; - } else { - this.showDocumentLevelSecurity = $scope.allowDocumentLevelSecurity; - this.showFieldLevelSecurity = $scope.allowFieldLevelSecurity; - } - }, - }; -}); diff --git a/x-pack/plugins/security/public/views/management/index_privileges_form/index_privileges_form.less b/x-pack/plugins/security/public/views/management/index_privileges_form/index_privileges_form.less deleted file mode 100644 index edd7a4898f45a..0000000000000 --- a/x-pack/plugins/security/public/views/management/index_privileges_form/index_privileges_form.less +++ /dev/null @@ -1,7 +0,0 @@ -.indexPrivilegesForm { - height: 550px; -} - -.indexPrivilegesList { - flex: 0 0 400px; -} diff --git a/x-pack/plugins/security/public/views/management/management.js b/x-pack/plugins/security/public/views/management/management.js index b2bd09e67e9bf..f7000478cbfc7 100644 --- a/x-pack/plugins/security/public/views/management/management.js +++ b/x-pack/plugins/security/public/views/management/management.js @@ -5,12 +5,11 @@ */ import 'plugins/security/views/management/change_password_form/change_password_form'; -import 'plugins/security/views/management/index_privileges_form/index_privileges_form'; import 'plugins/security/views/management/password_form/password_form'; import 'plugins/security/views/management/users'; import 'plugins/security/views/management/roles'; import 'plugins/security/views/management/edit_user'; -import 'plugins/security/views/management/edit_role'; +import 'plugins/security/views/management/edit_role/index'; import 'plugins/security/views/management/management.less'; import routes from 'ui/routes'; import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; diff --git a/x-pack/plugins/security/public/views/management/management.less b/x-pack/plugins/security/public/views/management/management.less index d9e16650aeecc..14fc8f4fa1324 100644 --- a/x-pack/plugins/security/public/views/management/management.less +++ b/x-pack/plugins/security/public/views/management/management.less @@ -1,5 +1,4 @@ @import '~plugins/xpack_main/style/main.less'; -@import './index_privileges_form/index_privileges_form'; .kuiFormFooter { display: flex; diff --git a/x-pack/plugins/security/server/lib/check_user_permission.js b/x-pack/plugins/security/server/lib/check_user_permission.js deleted file mode 100644 index 6890a6fc79092..0000000000000 --- a/x-pack/plugins/security/server/lib/check_user_permission.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. - * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */ - - -export async function checkUserPermission(permission, hasPermission) { - // POC Stub. TODO: Use ES Permissions API once implemented. - return Promise.resolve(hasPermission); -} diff --git a/x-pack/plugins/security/server/lib/privileges/privileges.js b/x-pack/plugins/security/server/lib/privileges/privileges.js index e05be3117f934..129d4b3abfb3e 100644 --- a/x-pack/plugins/security/server/lib/privileges/privileges.js +++ b/x-pack/plugins/security/server/lib/privileges/privileges.js @@ -45,7 +45,7 @@ function buildSavedObjectsReadPrivileges() { } function buildSavedObjectsPrivileges(actions) { - const objectTypes = ['config', 'dashboard', 'index-pattern', 'search', 'visualization', 'graph-workspace']; + const objectTypes = ['config', 'dashboard', 'graph-workspace', 'index-pattern', 'search', 'timelion-sheet', 'url', 'visualization']; return objectTypes .map(type => actions.map(action => `action:saved-objects/${type}/${action}`)) .reduce((acc, types) => [...acc, ...types], []); diff --git a/x-pack/plugins/spaces/common/index.js b/x-pack/plugins/spaces/common/index.js new file mode 100644 index 0000000000000..1cd6907d3d64c --- /dev/null +++ b/x-pack/plugins/spaces/common/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { isReservedSpace } from './is_reserved_space'; \ No newline at end of file diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index 603b8ddb22e11..38d2e84cc992f 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -52,7 +52,7 @@ export const spaces = (kibana) => new kibana.Plugin({ valid: true, space: await getActiveSpace(request.getSavedObjectsClient(), request.getBasePath()) }; - } catch(e) { + } catch (e) { vars.activeSpace = { valid: false, error: wrapError(e).output.payload diff --git a/x-pack/plugins/spaces/public/views/management/components/__snapshots__/page_header.test.js.snap b/x-pack/plugins/spaces/public/views/management/components/__snapshots__/page_header.test.js.snap index 936f678055d41..c7cbfdb53fd73 100644 --- a/x-pack/plugins/spaces/public/views/management/components/__snapshots__/page_header.test.js.snap +++ b/x-pack/plugins/spaces/public/views/management/components/__snapshots__/page_header.test.js.snap @@ -1,15 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`it renders without crashing 1`] = ` -
-
+ -
-
-
+ + `; diff --git a/x-pack/plugins/spaces/public/views/management/components/manage_space_page.js b/x-pack/plugins/spaces/public/views/management/components/manage_space_page.js index 2262e597fb433..2207076c3e695 100644 --- a/x-pack/plugins/spaces/public/views/management/components/manage_space_page.js +++ b/x-pack/plugins/spaces/public/views/management/components/manage_space_page.js @@ -25,6 +25,8 @@ import { DeleteSpacesButton } from './delete_spaces_button'; import { Notifier, toastNotifications } from 'ui/notify'; import { UrlContext } from './url_context'; import { toUrlContext, isValidUrlContext } from '../lib/url_context_utils'; +import { isReservedSpace } from '../../../../common'; +import { ReservedSpaceBadge } from './reserved_space_badge'; export class ManageSpacePage extends React.Component { state = { @@ -71,12 +73,7 @@ export class ManageSpacePage extends React.Component { - - -

{this.getTitle()}

-
- {this.getActionButton()} -
+ {this.getFormHeading()} @@ -108,7 +105,7 @@ export class ManageSpacePage extends React.Component { @@ -128,6 +125,19 @@ export class ManageSpacePage extends React.Component { ); } + getFormHeading = () => { + const isReserved = isReservedSpace(this.state.space); + + return ( + + +

{this.getTitle()}

+
+ {isReserved ? this.getReservedBadge() : this.getActionButton()} +
+ ); + }; + getTitle = () => { if (this.editingExistingSpace()) { return `Edit space`; @@ -135,8 +145,10 @@ export class ManageSpacePage extends React.Component { return `Create a space`; }; + getReservedBadge = () => ; + getActionButton = () => { - if (this.editingExistingSpace()) { + if (this.editingExistingSpace() && !isReservedSpace(this.state.space)) { return ( - - {this.props.breadcrumbs.map(this.buildBreadcrumb)} - + ); } buildBreadcrumb = (breadcrumb) => { - return ( - - {breadcrumb.display} - - ); + return { + text: breadcrumb.display, + href: breadcrumb.href, + }; } } - - PageHeader.propTypes = { breadcrumbs: PropTypes.array.isRequired -}; +}; \ No newline at end of file diff --git a/x-pack/plugins/spaces/public/views/management/components/page_header.test.js b/x-pack/plugins/spaces/public/views/management/components/page_header.test.js index b960175b38deb..706a6de4a0abc 100644 --- a/x-pack/plugins/spaces/public/views/management/components/page_header.test.js +++ b/x-pack/plugins/spaces/public/views/management/components/page_header.test.js @@ -6,30 +6,28 @@ import React from 'react'; import { PageHeader } from './page_header'; -import { render } from 'enzyme'; -import renderer from 'react-test-renderer'; +import { mount, shallow } from 'enzyme'; test('it renders without crashing', () => { - const component = renderer.create( + const component = shallow( ); expect(component).toMatchSnapshot(); }); test('it renders breadcrumbs', () => { - const component = render( + const component = mount( ); - expect(component.find('a')).toHaveLength(2); + expect(component.find('a.euiBreadcrumb')).toHaveLength(1); + expect(component.find('span.euiBreadcrumb')).toHaveLength(1); }); diff --git a/x-pack/plugins/spaces/public/views/management/components/reserved_space_badge.js b/x-pack/plugins/spaces/public/views/management/components/reserved_space_badge.js new file mode 100644 index 0000000000000..0ea7e05d77e12 --- /dev/null +++ b/x-pack/plugins/spaces/public/views/management/components/reserved_space_badge.js @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { isReservedSpace } from '../../../../common'; +import { + EuiBadge, + EuiToolTip, + EuiFlexItem, +} from '@elastic/eui'; + + +export const ReservedSpaceBadge = (props) => { + const { + space + } = props; + + if (isReservedSpace(space)) { + return ( + + + Reserved Space + + + ); + } + return null; +}; + +ReservedSpaceBadge.propTypes = { + space: PropTypes.object.isRequired +}; diff --git a/x-pack/plugins/spaces/public/views/management/components/reserved_space_badge.test.js b/x-pack/plugins/spaces/public/views/management/components/reserved_space_badge.test.js new file mode 100644 index 0000000000000..e45926b859824 --- /dev/null +++ b/x-pack/plugins/spaces/public/views/management/components/reserved_space_badge.test.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiBadge +} from '@elastic/eui'; +import { ReservedSpaceBadge } from './reserved_space_badge'; +import { + shallow +} from 'enzyme'; + +const reservedSpace = { + _reserved: true +}; + +const unreservedSpace = {}; + +test('it renders without crashing', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiBadge)).toHaveLength(1); +}); + +test('it renders nothing for an unreserved space', () => { + const wrapper = shallow(); + expect(wrapper.find('*')).toHaveLength(0); +}); diff --git a/x-pack/plugins/spaces/public/views/management/components/url_context.js b/x-pack/plugins/spaces/public/views/management/components/url_context.js index 418a51b29347e..6253e36d0f9e2 100644 --- a/x-pack/plugins/spaces/public/views/management/components/url_context.js +++ b/x-pack/plugins/spaces/public/views/management/components/url_context.js @@ -35,7 +35,7 @@ export class UrlContext extends Component {
this.textFieldRef = ref} @@ -48,6 +48,10 @@ export class UrlContext extends Component { } getLabel = () => { + if (!this.props.editable) { + return (

URL Context

); + } + const editLinkText = this.state.editing ? `[stop editing]` : `[edit]`; return (

URL Context {editLinkText}

); }; diff --git a/x-pack/plugins/spaces/public/views/management/manage_spaces.less b/x-pack/plugins/spaces/public/views/management/manage_spaces.less index a9696d0c2f824..ae515cbb829fa 100644 --- a/x-pack/plugins/spaces/public/views/management/manage_spaces.less +++ b/x-pack/plugins/spaces/public/views/management/manage_spaces.less @@ -1,7 +1,7 @@ -.application, .euiPanel { +.manageSpaces__application, .manageSpaces__.euiPanel { background: #f5f5f5 } -.euiPage { +.manageSpaces__euiPage { padding: 0; } diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index fb4184fbbb632..7c39d6e8fae06 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -10,9 +10,9 @@ esutils "^2.0.2" js-tokens "^3.0.0" -"@elastic/eui@0.0.47": - version "0.0.47" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-0.0.47.tgz#5bae27966bb1d68bb3106853610a407509053b44" +"@elastic/eui@v0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-0.0.51.tgz#5d809af270dd9994a609fd01eaa84e21a62fff98" dependencies: brace "^0.10.0" classnames "^2.2.5" @@ -990,8 +990,8 @@ brace@0.10.0, brace@^0.10.0: w3c-blob "0.0.1" brace@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.0.tgz#155cd80607687dc8cb908f0df94e62a033c1d563" + version "0.11.1" + resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" braces@^1.8.2: version "1.8.5" @@ -1072,6 +1072,10 @@ buffer-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" +buffer-from@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04" + buffer@^3.0.1: version "3.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-3.6.0.tgz#a72c936f77b96bf52f5f7e7b467180628551defb" @@ -1400,7 +1404,16 @@ concat-stream@1.5.1: readable-stream "~2.0.0" typedarray "~0.0.5" -concat-stream@^1.4.7, concat-stream@~1.6.0: +concat-stream@^1.4.7: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-stream@~1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" dependencies: @@ -1443,8 +1456,8 @@ core-js@^2.4.0, core-js@^2.5.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" core-js@^2.5.1: - version "2.5.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b" + version "2.5.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -2470,8 +2483,8 @@ focus-trap-react@^3.0.4, focus-trap-react@^3.1.1: focus-trap "^2.0.1" focus-trap@^2.0.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-2.4.2.tgz#44ea1c55a9c22c2b6529dcebbde6390eb2ee4c88" + version "2.4.5" + resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-2.4.5.tgz#91c9c9ffb907f8f4446d80202dda9c12c2853ddb" dependencies: tabbable "^1.0.3" @@ -3316,10 +3329,16 @@ icalendar@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/icalendar/-/icalendar-0.7.1.tgz#d0d3486795f8f1c5cf4f8cafac081b4b4e7a32ae" -iconv-lite@0.4.19, iconv-lite@^0.4.19, iconv-lite@~0.4.13: +iconv-lite@0.4.19, iconv-lite@^0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" +iconv-lite@~0.4.13: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + dependencies: + safer-buffer ">= 2.1.2 < 3" + ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" @@ -4513,7 +4532,11 @@ lodash@3.10.1, lodash@^3.10.0, lodash@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1: +lodash@^4.0.1: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + +lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -5539,7 +5562,7 @@ prop-types@15.5.8: dependencies: fbjs "^0.8.9" -prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0: +prop-types@^15.5.0, prop-types@^15.5.4, prop-types@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: @@ -5547,7 +5570,7 @@ prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.6.1: +prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1: version "15.6.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" dependencies: @@ -5710,8 +5733,8 @@ react-clipboard.js@^1.1.2: prop-types "^15.5.0" react-color@^2.13.8: - version "2.13.8" - resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.13.8.tgz#bcc58f79a722b9bfc37c402e68cd18f26970aee4" + version "2.14.1" + resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.14.1.tgz#db8ad4f45d81e74896fc2e1c99508927c6d084e0" dependencies: lodash "^4.0.1" material-colors "^1.2.1" @@ -5743,6 +5766,10 @@ react-input-autosize@^2.1.2, react-input-autosize@^2.2.1: dependencies: prop-types "^15.5.8" +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + react-markdown-renderer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/react-markdown-renderer/-/react-markdown-renderer-1.4.0.tgz#f3b95bd9fc7f7bf8ab3f0150aa696b41740e7d01" @@ -5855,14 +5882,15 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.2.0: prop-types "^15.6.0" react-virtualized@^9.18.5: - version "9.18.5" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.18.5.tgz#42dd390ebaa7ea809bfcaf775d39872641679b89" + version "9.19.1" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.19.1.tgz#84b53253df2d9df61c85ce037141edccc70a73fd" dependencies: babel-runtime "^6.26.0" classnames "^2.2.3" dom-helpers "^2.4.0 || ^3.0.0" loose-envify "^1.3.0" prop-types "^15.6.0" + react-lifecycles-compat "^3.0.4" react-vis@^1.8.1: version "1.8.2" @@ -5924,7 +5952,7 @@ read-pkg@^1.0.0: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -5936,6 +5964,18 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable string_decoder "~1.0.3" util-deprecate "~1.0.1" +readable-stream@^2.2.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@^2.3.3, readable-stream@^2.3.5: version "2.3.5" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" @@ -6335,10 +6375,18 @@ rxjs@5.3.0: dependencies: symbol-observable "^1.0.1" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + samsam@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" @@ -6739,6 +6787,12 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -6898,8 +6952,8 @@ tabbable@1.1.0: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.0.tgz#2c9a9c9f09db5bb0659f587d532548dd6ef2067b" tabbable@^1.0.3, tabbable@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.2.tgz#b171680aea6e0a3e9281ff23532e2e5de11c0d94" + version "1.1.3" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.3.tgz#0e4ee376f3631e42d7977a074dbd2b3827843081" tar-fs@1.13.0: version "1.13.0" @@ -7160,8 +7214,8 @@ typedarray@^0.0.6, typedarray@~0.0.5: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" ua-parser-js@^0.7.9: - version "0.7.17" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" + version "0.7.18" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" uglify-js@^2.6: version "2.8.29" @@ -7293,10 +7347,14 @@ uuid@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" -uuid@^3.0.0, uuid@^3.1.0: +uuid@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" +uuid@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + v8flags@^2.0.2: version "2.1.1" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" @@ -7489,8 +7547,8 @@ whatwg-encoding@^1.0.1: iconv-lite "0.4.19" whatwg-fetch@>=0.10.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + version "2.0.4" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" whatwg-url@^6.3.0: version "6.4.0" diff --git a/yarn.lock b/yarn.lock index 2098dda85233c..459cd99a1d3ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -77,9 +77,9 @@ version "0.0.0" uid "" -"@elastic/eui@0.0.47", "@elastic/eui@v0.0.47": - version "0.0.47" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-0.0.47.tgz#5bae27966bb1d68bb3106853610a407509053b44" +"@elastic/eui@v0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-0.0.51.tgz#5d809af270dd9994a609fd01eaa84e21a62fff98" dependencies: brace "^0.10.0" classnames "^2.2.5" @@ -1860,6 +1860,10 @@ buffer-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" +buffer-from@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04" + buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -2576,7 +2580,16 @@ concat-stream@1.6.0: readable-stream "^2.2.2" typedarray "^0.0.6" -concat-stream@^1.4.7, concat-stream@^1.5.2, concat-stream@^1.6.0, concat-stream@~1.6.0: +concat-stream@^1.4.7: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-stream@^1.5.2, concat-stream@^1.6.0, concat-stream@~1.6.0: version "1.6.1" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.1.tgz#261b8f518301f1d834e36342b9fea095d2620a26" dependencies: @@ -2699,10 +2712,14 @@ core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" -core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1: +core-js@^2.2.0, core-js@^2.5.0: version "2.5.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" +core-js@^2.4.0, core-js@^2.5.1: + version "2.5.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -4772,8 +4789,8 @@ focus-trap-react@^3.0.4, focus-trap-react@^3.1.1: focus-trap "^2.0.1" focus-trap@^2.0.1: - version "2.4.3" - resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-2.4.3.tgz#95edc23e77829b7772cb2486d61fd6371ce112f9" + version "2.4.5" + resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-2.4.5.tgz#91c9c9ffb907f8f4446d80202dda9c12c2853ddb" dependencies: tabbable "^1.0.3" @@ -5905,7 +5922,7 @@ icalendar@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/icalendar/-/icalendar-0.7.1.tgz#d0d3486795f8f1c5cf4f8cafac081b4b4e7a32ae" -iconv-lite@0.4, iconv-lite@0.4.19, iconv-lite@^0.4.13, iconv-lite@^0.4.17, iconv-lite@^0.4.19, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@0.4.19, iconv-lite@^0.4.13, iconv-lite@^0.4.17, iconv-lite@^0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -5917,6 +5934,12 @@ iconv-lite@0.4.7: version "0.4.7" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.7.tgz#89d32fec821bf8597f44609b4bc09bed5c209a23" +iconv-lite@~0.4.13: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + dependencies: + safer-buffer ">= 2.1.2 < 3" + icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -10041,7 +10064,16 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-ace@^5.5.0, react-ace@^5.9.0: +react-ace@^5.5.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.10.0.tgz#e328b37ac52759f700be5afdb86ada2f5ec84c5e" + dependencies: + brace "^0.11.0" + lodash.get "^4.4.2" + lodash.isequal "^4.1.1" + prop-types "^15.5.8" + +react-ace@^5.9.0: version "5.9.0" resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.9.0.tgz#427a1cc4869b960a6f9748aa7eb169a9269fc336" dependencies: @@ -10129,6 +10161,10 @@ react-input-range@^1.3.0: autobind-decorator "^1.3.4" prop-types "^15.5.8" +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + react-markdown-renderer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/react-markdown-renderer/-/react-markdown-renderer-1.4.0.tgz#f3b95bd9fc7f7bf8ab3f0150aa696b41740e7d01" @@ -10271,14 +10307,15 @@ react-toggle@4.0.2: classnames "^2.2.5" react-virtualized@^9.18.5: - version "9.18.5" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.18.5.tgz#42dd390ebaa7ea809bfcaf775d39872641679b89" + version "9.19.1" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.19.1.tgz#84b53253df2d9df61c85ce037141edccc70a73fd" dependencies: babel-runtime "^6.26.0" classnames "^2.2.3" dom-helpers "^2.4.0 || ^3.0.0" loose-envify "^1.3.0" prop-types "^15.6.0" + react-lifecycles-compat "^3.0.4" react-vis@^1.8.1: version "1.9.2" @@ -10381,7 +10418,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@~2.3.3: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@~2.3.3: version "2.3.5" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" dependencies: @@ -10393,6 +10430,18 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable string_decoder "~1.0.3" util-deprecate "~1.0.1" +readable-stream@^2.2.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@~1.0.2: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -10928,10 +10977,14 @@ rxjs@5.4.3: dependencies: symbol-observable "^1.0.1" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + safe-json-stringify@~1: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz#bd2b6dad1ebafab3c24672a395527f01804b7e19" @@ -10949,6 +11002,10 @@ safefs@^4.0.0: editions "^1.1.1" graceful-fs "^4.1.4" +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + samsam@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" @@ -11622,6 +11679,12 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -11820,8 +11883,8 @@ tabbable@1.1.0: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.0.tgz#2c9a9c9f09db5bb0659f587d532548dd6ef2067b" tabbable@^1.0.3, tabbable@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.2.tgz#b171680aea6e0a3e9281ff23532e2e5de11c0d94" + version "1.1.3" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.3.tgz#0e4ee376f3631e42d7977a074dbd2b3827843081" table@^3.7.8: version "3.8.3" @@ -12276,8 +12339,8 @@ typescript@^2.8.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.1.tgz#6160e4f8f195d5ba81d4876f9c0cc1fbc0820624" ua-parser-js@^0.7.9: - version "0.7.17" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" + version "0.7.18" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.5" @@ -13116,7 +13179,11 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: dependencies: iconv-lite "0.4.19" -whatwg-fetch@>=0.10.0, whatwg-fetch@^2.0.3: +whatwg-fetch@>=0.10.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" + +whatwg-fetch@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"