diff options
-rw-r--r-- | tests/unit/test_web.py | 1 | ||||
-rw-r--r-- | web/src/App.jsx | 47 | ||||
-rw-r--r-- | web/src/actions/configErrors.js | 12 | ||||
-rw-r--r-- | web/src/actions/user.js | 10 | ||||
-rw-r--r-- | web/src/api.js | 179 | ||||
-rw-r--r-- | web/src/containers/auth/Auth.jsx | 8 | ||||
-rw-r--r-- | web/src/containers/autohold/AutoholdTable.jsx | 2 | ||||
-rw-r--r-- | web/src/containers/autohold/autoholdModal.jsx | 4 | ||||
-rw-r--r-- | web/src/containers/build/Buildset.jsx | 4 | ||||
-rw-r--r-- | web/src/containers/status/Change.jsx | 10 | ||||
-rw-r--r-- | web/src/pages/AuthCallback.jsx | 1 | ||||
-rw-r--r-- | web/src/pages/AuthRequired.jsx | 44 | ||||
-rw-r--r-- | web/src/reducers/configErrors.js | 10 | ||||
-rw-r--r-- | web/src/reducers/notifications.js | 1 | ||||
-rw-r--r-- | web/src/reducers/tenant.js | 4 | ||||
-rw-r--r-- | web/src/reducers/user.js | 10 | ||||
-rwxr-xr-x | zuul/web/__init__.py | 15 |
17 files changed, 242 insertions, 120 deletions
diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py index c9abf7f1c..59cadf288 100644 --- a/tests/unit/test_web.py +++ b/tests/unit/test_web.py @@ -3551,6 +3551,7 @@ class TestWebApiAccessRules(BaseTestWeb): '/api/connections', '/api/components', '/api/tenants', + '/api/authorizations', '/api/tenant/{tenant}/status', '/api/tenant/{tenant}/status/change/{change}', '/api/tenant/{tenant}/jobs', diff --git a/web/src/App.jsx b/web/src/App.jsx index 4762175c6..6a5c6e010 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -20,6 +20,7 @@ import PropTypes from 'prop-types' import { matchPath, withRouter } from 'react-router' import { Link, NavLink, Redirect, Route, Switch } from 'react-router-dom' import { connect } from 'react-redux' +import { withAuth } from 'oidc-react' import { TimedToastNotification, ToastNotificationList, @@ -65,17 +66,19 @@ import SelectTz from './containers/timezone/SelectTz' import ConfigModal from './containers/config/Config' import logo from './images/logo.svg' import { clearNotification } from './actions/notifications' -import { fetchConfigErrorsAction } from './actions/configErrors' +import { fetchConfigErrorsAction, clearConfigErrorsAction } from './actions/configErrors' import { routes } from './routes' import { setTenantAction } from './actions/tenant' import { configureAuthFromTenant, configureAuthFromInfo } from './actions/auth' import { getHomepageUrl } from './api' import AuthCallbackPage from './pages/AuthCallback' +import AuthRequiredPage from './pages/AuthRequired' class App extends React.Component { static propTypes = { notifications: PropTypes.array, configErrors: PropTypes.array, + configErrorsReady: PropTypes.bool, info: PropTypes.object, tenant: PropTypes.object, timezone: PropTypes.string, @@ -85,6 +88,7 @@ class App extends React.Component { isKebabDropdownOpen: PropTypes.bool, user: PropTypes.object, auth: PropTypes.object, + signIn: PropTypes.func, } state = { @@ -116,8 +120,17 @@ class App extends React.Component { } } + isAuthReady() { + const { info, auth, user } = this.props + return !(info.isFetching || + !auth.info || + auth.isFetching || + (user.data && user.data.isFetching) || + user.isFetching) + } + renderContent = () => { - const { info, tenant, auth } = this.props + const { tenant, auth, user } = this.props const allRoutes = [] if ((window.location.origin + window.location.pathname) === @@ -126,9 +139,19 @@ class App extends React.Component { // validation is complete (it will internally redirect when complete) return <AuthCallbackPage/> } - if (info.isFetching || !auth.info || auth.isFetching) { + if (!this.isAuthReady()) { + return <Fetching /> + } + if (auth.info.read_protected && !user.data) { + console.log('Read-access login required') + const redirect_target = window.location.href.slice(getHomepageUrl().length) + localStorage.setItem('zuul_auth_redirect', redirect_target) + this.props.signIn() return <Fetching /> } + if (auth.info.read_protected && user.scope.length<1) { + return <AuthRequiredPage/> + } this.menu // Do not include '/tenants' route in white-label setup .filter(item => @@ -164,9 +187,10 @@ class App extends React.Component { componentDidUpdate() { // This method is called when info property is updated - const { tenant, info } = this.props + const { tenant, info, auth, user, configErrorsReady } = this.props if (info.ready) { - let tenantName, whiteLabel + let tenantName = null + let whiteLabel if (info.tenant) { // White label @@ -188,7 +212,7 @@ class App extends React.Component { const tenantAction = setTenantAction(tenantName, whiteLabel) this.props.dispatch(tenantAction) if (tenantName) { - this.props.dispatch(fetchConfigErrorsAction(tenantAction.tenant)) + this.props.dispatch(clearConfigErrorsAction()) } if (whiteLabel || !tenantName) { // The app info endpoint was already a tenant info @@ -199,6 +223,12 @@ class App extends React.Component { this.props.dispatch(configureAuthFromTenant(tenantName)) } } + if (tenant && tenant.name && !configErrorsReady && this.isAuthReady() && + (!auth.info.read_protected || user.data)) { + // This will happen after the tenant action is complete, so we + // can use the "old" tenant now. + this.props.dispatch(fetchConfigErrorsAction(tenant)) + } } } @@ -487,11 +517,12 @@ class App extends React.Component { export default withRouter(connect( state => ({ notifications: state.notifications, - configErrors: state.configErrors, + configErrors: state.configErrors.errors, + configErrorsReady: state.configErrors.ready, info: state.info, tenant: state.tenant, timezone: state.timezone, user: state.user, auth: state.auth, }) -)(App)) +)(withAuth(App))) diff --git a/web/src/actions/configErrors.js b/web/src/actions/configErrors.js index e4b17d801..a3baeac4c 100644 --- a/web/src/actions/configErrors.js +++ b/web/src/actions/configErrors.js @@ -18,11 +18,19 @@ export function fetchConfigErrorsAction (tenant) { return (dispatch) => { return fetchConfigErrors(tenant.apiPrefix) .then(response => { - dispatch({type: 'FETCH_CONFIGERRORS_SUCCESS', + dispatch({type: 'CONFIGERRORS_FETCH_SUCCESS', errors: response.data}) }) .catch(error => { - throw (error) + dispatch({type: 'CONFIGERRORS_FETCH_FAIL', + error}) + }) } } + +export function clearConfigErrorsAction () { + return (dispatch) => { + dispatch({type: 'CONFIGERRORS_CLEAR'}) + } +} diff --git a/web/src/actions/user.js b/web/src/actions/user.js index d01d4c24c..8c88187ad 100644 --- a/web/src/actions/user.js +++ b/web/src/actions/user.js @@ -37,10 +37,12 @@ export const fetchUserACLRequest = (tenant) => ({ }) export const userLoggedIn = (user, redirect) => (dispatch) => { + const token = getToken(user) + API.setAuthToken(token) dispatch({ type: USER_LOGGED_IN, user: user, - token: getToken(user), + token: token, redirect: redirect, }) } @@ -62,10 +64,10 @@ const fetchUserACLFail = error => ({ error }) -export const fetchUserACL = (tenant, user) => (dispatch) => { +export const fetchUserACL = (tenant) => (dispatch) => { dispatch(fetchUserACLRequest(tenant)) - let apiPrefix = 'tenant/' + tenant + '/' - return API.fetchUserAuthorizations(apiPrefix, user.token) + let apiPrefix = tenant? 'tenant/' + tenant + '/' : '' + return API.fetchUserAuthorizations(apiPrefix) .then(response => dispatch(fetchUserACLSuccess(response.data))) .catch(error => { dispatch(fetchUserACLFail(error)) diff --git a/web/src/api.js b/web/src/api.js index de2275c7c..fc63317a9 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -14,6 +14,12 @@ import Axios from 'axios' +let authToken = undefined + +export function setAuthToken(token) { + authToken = token +} + function getHomepageUrl(url) { // // Discover serving location from href. @@ -103,14 +109,25 @@ function getStreamUrl(apiPrefix) { return streamUrl } -function getWithCorsHandling(url) { +function makeRequest(url, method, data) { + if (method === undefined) { + method = 'get' + } + // This performs a simple GET and tries to detect if CORS errors are // due to proxy authentication errors. const instance = Axios.create({ baseURL: apiUrl }) + + if (authToken) { + instance.defaults.headers.common['Authorization'] = 'Bearer ' + authToken + } + + const config = {method, url, data} + // First try the request as normal - let res = instance.get(url).catch(err => { + let res = instance.request(config).catch(err => { if (err.response === undefined) { // This is either a Network, DNS, or CORS error, but we can't tell which. // If we're behind an authz proxy, it's possible our creds have timed out @@ -119,7 +136,7 @@ function getWithCorsHandling(url) { // issuing a redirect if X-Requested-With is set to 'XMLHttpRequest' and // will instead issue a 403. We can use this to detect that case. instance.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' - let res2 = instance.get(url).catch(err2 => { + let res2 = instance.request(config).catch(err2 => { if (err2.response && err2.response.status === 403) { // We might be getting a redirect or something else, // so reload the page. @@ -133,165 +150,165 @@ function getWithCorsHandling(url) { }) return res2 } + throw (err) }) return res } // Direct APIs function fetchInfo() { - return getWithCorsHandling('info') + return makeRequest('info') } function fetchComponents() { - return getWithCorsHandling('components') + return makeRequest('components') } function fetchTenantInfo(apiPrefix) { - return getWithCorsHandling(apiPrefix + 'info') + return makeRequest(apiPrefix + 'info') } + function fetchOpenApi() { return Axios.get(getHomepageUrl() + 'openapi.yaml') } + function fetchTenants() { - return getWithCorsHandling(apiUrl + 'tenants') + return makeRequest(apiUrl + 'tenants') } + function fetchConfigErrors(apiPrefix) { - return getWithCorsHandling(apiPrefix + 'config-errors') + return makeRequest(apiPrefix + 'config-errors') } + function fetchStatus(apiPrefix) { - return getWithCorsHandling(apiPrefix + 'status') + return makeRequest(apiPrefix + 'status') } + function fetchChangeStatus(apiPrefix, changeId) { - return getWithCorsHandling(apiPrefix + 'status/change/' + changeId) + return makeRequest(apiPrefix + 'status/change/' + changeId) } + function fetchFreezeJob(apiPrefix, pipelineName, projectName, branchName, jobName) { - return getWithCorsHandling(apiPrefix + + return makeRequest(apiPrefix + 'pipeline/' + pipelineName + '/project/' + projectName + '/branch/' + branchName + '/freeze-job/' + jobName) } + function fetchBuild(apiPrefix, buildId) { - return getWithCorsHandling(apiPrefix + 'build/' + buildId) + return makeRequest(apiPrefix + 'build/' + buildId) } + function fetchBuilds(apiPrefix, queryString) { let path = 'builds' if (queryString) { path += '?' + queryString.slice(1) } - return getWithCorsHandling(apiPrefix + path) + return makeRequest(apiPrefix + path) } + function fetchBuildset(apiPrefix, buildsetId) { - return getWithCorsHandling(apiPrefix + 'buildset/' + buildsetId) + return makeRequest(apiPrefix + 'buildset/' + buildsetId) } + function fetchBuildsets(apiPrefix, queryString) { let path = 'buildsets' if (queryString) { path += '?' + queryString.slice(1) } - return getWithCorsHandling(apiPrefix + path) + return makeRequest(apiPrefix + path) } + function fetchPipelines(apiPrefix) { - return getWithCorsHandling(apiPrefix + 'pipelines') + return makeRequest(apiPrefix + 'pipelines') } + function fetchProject(apiPrefix, projectName) { - return getWithCorsHandling(apiPrefix + 'project/' + projectName) + return makeRequest(apiPrefix + 'project/' + projectName) } + function fetchProjects(apiPrefix) { - return getWithCorsHandling(apiPrefix + 'projects') + return makeRequest(apiPrefix + 'projects') } + function fetchJob(apiPrefix, jobName) { - return getWithCorsHandling(apiPrefix + 'job/' + jobName) + return makeRequest(apiPrefix + 'job/' + jobName) } + function fetchJobGraph(apiPrefix, projectName, pipelineName, branchName) { - return getWithCorsHandling(apiPrefix + + return makeRequest(apiPrefix + 'pipeline/' + pipelineName + '/project/' + projectName + '/branch/' + branchName + '/freeze-jobs') } + function fetchJobs(apiPrefix) { - return getWithCorsHandling(apiPrefix + 'jobs') + return makeRequest(apiPrefix + 'jobs') } + function fetchLabels(apiPrefix) { - return getWithCorsHandling(apiPrefix + 'labels') + return makeRequest(apiPrefix + 'labels') } + function fetchNodes(apiPrefix) { - return getWithCorsHandling(apiPrefix + 'nodes') + return makeRequest(apiPrefix + 'nodes') } + function fetchSemaphores(apiPrefix) { - return Axios.get(apiUrl + apiPrefix + 'semaphores') + return makeRequest(apiPrefix + 'semaphores') } + function fetchAutoholds(apiPrefix) { - return getWithCorsHandling(apiPrefix + 'autohold') + return makeRequest(apiPrefix + 'autohold') } + function fetchAutohold(apiPrefix, requestId) { - return getWithCorsHandling(apiPrefix + 'autohold/' + requestId) + return makeRequest(apiPrefix + 'autohold/' + requestId) } -// token-protected API -function fetchUserAuthorizations(apiPrefix, token) { - // Axios.defaults.headers.common['Authorization'] = 'Bearer ' + token - const instance = Axios.create({ - baseURL: apiUrl - }) - instance.defaults.headers.common['Authorization'] = 'Bearer ' + token - let res = instance.get(apiPrefix + 'authorizations') - .catch(err => { console.log('An error occurred', err) }) - // Axios.defaults.headers.common['Authorization'] = '' - return res +function fetchUserAuthorizations(apiPrefix) { + return makeRequest(apiPrefix + 'authorizations') } -function dequeue(apiPrefix, projectName, pipeline, change, token) { - const instance = Axios.create({ - baseURL: apiUrl - }) - instance.defaults.headers.common['Authorization'] = 'Bearer ' + token - let res = instance.post( +function dequeue(apiPrefix, projectName, pipeline, change) { + return makeRequest( apiPrefix + 'project/' + projectName + '/dequeue', + 'post', { pipeline: pipeline, change: change, } ) - return res } -function dequeue_ref(apiPrefix, projectName, pipeline, ref, token) { - const instance = Axios.create({ - baseURL: apiUrl - }) - instance.defaults.headers.common['Authorization'] = 'Bearer ' + token - let res = instance.post( + +function dequeue_ref(apiPrefix, projectName, pipeline, ref) { + return makeRequest( apiPrefix + 'project/' + projectName + '/dequeue', + 'post', { pipeline: pipeline, ref: ref, } ) - return res } -function enqueue(apiPrefix, projectName, pipeline, change, token) { - const instance = Axios.create({ - baseURL: apiUrl - }) - instance.defaults.headers.common['Authorization'] = 'Bearer ' + token - let res = instance.post( +function enqueue(apiPrefix, projectName, pipeline, change) { + return makeRequest( apiPrefix + 'project/' + projectName + '/enqueue', + 'post', { pipeline: pipeline, change: change, } ) - return res } -function enqueue_ref(apiPrefix, projectName, pipeline, ref, oldrev, newrev, token) { - const instance = Axios.create({ - baseURL: apiUrl - }) - instance.defaults.headers.common['Authorization'] = 'Bearer ' + token - let res = instance.post( + +function enqueue_ref(apiPrefix, projectName, pipeline, ref, oldrev, newrev) { + return makeRequest( apiPrefix + 'project/' + projectName + '/enqueue', + 'post', { pipeline: pipeline, ref: ref, @@ -299,16 +316,13 @@ function enqueue_ref(apiPrefix, projectName, pipeline, ref, oldrev, newrev, toke newrev: newrev, } ) - return res } + function autohold(apiPrefix, projectName, job, change, ref, - reason, count, node_hold_expiration, token) { - const instance = Axios.create({ - baseURL: apiUrl - }) - instance.defaults.headers.common['Authorization'] = 'Bearer ' + token - let res = instance.post( + reason, count, node_hold_expiration) { + return makeRequest( apiPrefix + 'project/' + projectName + '/autohold', + 'post', { change: change, job: job, @@ -318,33 +332,24 @@ function autohold(apiPrefix, projectName, job, change, ref, node_hold_expiration: node_hold_expiration, } ) - return res } -function autohold_delete(apiPrefix, requestId, token) { - const instance = Axios.create({ - baseURL: apiUrl - }) - instance.defaults.headers.common['Authorization'] = 'Bearer ' + token - let res = instance.delete( - apiPrefix + '/autohold/' + requestId +function autohold_delete(apiPrefix, requestId) { + return makeRequest( + apiPrefix + '/autohold/' + requestId, + 'delete' ) - return res } -function promote(apiPrefix, pipeline, changes, token) { - const instance = Axios.create({ - baseURL: apiUrl - }) - instance.defaults.headers.common['Authorization'] = 'Bearer ' + token - let res = instance.post( +function promote(apiPrefix, pipeline, changes) { + return makeRequest( apiPrefix + '/promote', + 'post', { pipeline: pipeline, changes: changes, } ) - return res } diff --git a/web/src/containers/auth/Auth.jsx b/web/src/containers/auth/Auth.jsx index 177e9a02d..5bdde91fa 100644 --- a/web/src/containers/auth/Auth.jsx +++ b/web/src/containers/auth/Auth.jsx @@ -104,8 +104,14 @@ class AuthContainer extends React.Component { } componentDidMount() { + const { user, tenant } = this.props this.props.userManager.events.addAccessTokenExpired(this.onAccessTokenExpired) this.props.userManager.events.addUserLoaded(this.onUserLoaded) + + if (user.data) { + console.log('Refreshing ACL', user.tenant, tenant.name) + this.props.dispatch(fetchUserACL(tenant? tenant.name : null)) + } } componentWillUnmount() { @@ -119,7 +125,7 @@ class AuthContainer extends React.Component { // Make sure the token is current and the tenant is up to date. if (user.data && user.tenant !== tenant.name) { console.log('Refreshing ACL', user.tenant, tenant.name) - this.props.dispatch(fetchUserACL(tenant.name, user)) + this.props.dispatch(fetchUserACL(tenant? tenant.name : null)) } } diff --git a/web/src/containers/autohold/AutoholdTable.jsx b/web/src/containers/autohold/AutoholdTable.jsx index eda75a4df..749258272 100644 --- a/web/src/containers/autohold/AutoholdTable.jsx +++ b/web/src/containers/autohold/AutoholdTable.jsx @@ -87,7 +87,7 @@ function AutoholdTable(props) { ] function handleAutoholdDelete(requestId) { - autohold_delete(tenant.apiPrefix, requestId, user.token) + autohold_delete(tenant.apiPrefix, requestId) .then(() => { dispatch(addNotification( { diff --git a/web/src/containers/autohold/autoholdModal.jsx b/web/src/containers/autohold/autoholdModal.jsx index 522d8639b..5d4ff5208 100644 --- a/web/src/containers/autohold/autoholdModal.jsx +++ b/web/src/containers/autohold/autoholdModal.jsx @@ -64,7 +64,7 @@ const AutoholdModal = props => { let ah_change = change === '' ? null : change let ah_ref = changeRef === '' ? null : changeRef - autohold(tenant.apiPrefix, project, job_name, ah_change, ah_ref, reason, parseInt(count), parseInt(nodeHoldExpiration), user.token) + autohold(tenant.apiPrefix, project, job_name, ah_change, ah_ref, reason, parseInt(count), parseInt(nodeHoldExpiration)) .then(() => { /* TODO it looks like there is a delay in the registering of the autohold request by the backend, meaning we sometimes do not get the newly created request after @@ -209,4 +209,4 @@ AutoholdModal.propTypes = { export default connect((state) => ({ tenant: state.tenant, user: state.user, -}))(AutoholdModal)
\ No newline at end of file +}))(AutoholdModal) diff --git a/web/src/containers/build/Buildset.jsx b/web/src/containers/build/Buildset.jsx index 048a277bc..2ca70549d 100644 --- a/web/src/containers/build/Buildset.jsx +++ b/web/src/containers/build/Buildset.jsx @@ -181,7 +181,7 @@ function Buildset({ buildset, timezone, tenant, user }) { if (buildset.change === null) { const oldrev = '0000000000000000000000000000000000000000' const newrev = buildset.newrev ? buildset.newrev : '0000000000000000000000000000000000000000' - enqueue_ref(tenant.apiPrefix, buildset.project, buildset.pipeline, buildset.ref, oldrev, newrev, user.token) + enqueue_ref(tenant.apiPrefix, buildset.project, buildset.pipeline, buildset.ref, oldrev, newrev) .then(() => { dispatch(addNotification( { @@ -196,7 +196,7 @@ function Buildset({ buildset, timezone, tenant, user }) { }) } else { const changeId = buildset.change + ',' + buildset.patchset - enqueue(tenant.apiPrefix, buildset.project, buildset.pipeline, changeId, user.token) + enqueue(tenant.apiPrefix, buildset.project, buildset.pipeline, changeId) .then(() => { dispatch(addNotification( { diff --git a/web/src/containers/status/Change.jsx b/web/src/containers/status/Change.jsx index c90d6bdfd..ac0a4e6e8 100644 --- a/web/src/containers/status/Change.jsx +++ b/web/src/containers/status/Change.jsx @@ -58,14 +58,14 @@ class Change extends React.Component { } dequeueConfirm = () => { - const { tenant, user, change, pipeline } = this.props + const { tenant, change, pipeline } = this.props let projectName = change.project let changeId = change.id || 'N/A' let changeRef = change.ref this.setState(() => ({ showDequeueModal: false })) // post-merge if (/^[0-9a-f]{40}$/.test(changeId)) { - dequeue_ref(tenant.apiPrefix, projectName, pipeline.name, changeRef, user.token) + dequeue_ref(tenant.apiPrefix, projectName, pipeline.name, changeRef) .then(() => { this.props.dispatch(fetchStatusIfNeeded(tenant)) }) @@ -74,7 +74,7 @@ class Change extends React.Component { }) // pre-merge, ie we have a change id } else if (changeId !== 'N/A') { - dequeue(tenant.apiPrefix, projectName, pipeline.name, changeId, user.token) + dequeue(tenant.apiPrefix, projectName, pipeline.name, changeId) .then(() => { this.props.dispatch(fetchStatusIfNeeded(tenant)) }) @@ -118,11 +118,11 @@ class Change extends React.Component { } promoteConfirm = () => { - const { tenant, user, change, pipeline } = this.props + const { tenant, change, pipeline } = this.props let changeId = change.id || 'NA' this.setState(() => ({ showPromoteModal: false })) if (changeId !== 'N/A') { - promote(tenant.apiPrefix, pipeline.name, [changeId,], user.token) + promote(tenant.apiPrefix, pipeline.name, [changeId,]) .then(() => { this.props.dispatch(fetchStatusIfNeeded(tenant)) }) diff --git a/web/src/pages/AuthCallback.jsx b/web/src/pages/AuthCallback.jsx index c8f765be2..f964cd81f 100644 --- a/web/src/pages/AuthCallback.jsx +++ b/web/src/pages/AuthCallback.jsx @@ -1,4 +1,5 @@ // Copyright 2020 Red Hat, Inc +// Copyright 2022 Acme Gating, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain diff --git a/web/src/pages/AuthRequired.jsx b/web/src/pages/AuthRequired.jsx new file mode 100644 index 000000000..a14259716 --- /dev/null +++ b/web/src/pages/AuthRequired.jsx @@ -0,0 +1,44 @@ +// Copyright 2020 Red Hat, Inc +// Copyright 2022 Acme Gating, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +import React from 'react' +import { + EmptyState, + EmptyStateBody, + EmptyStateIcon, + Title, +} from '@patternfly/react-core' +import { + LockIcon, +} from '@patternfly/react-icons' + +function AuthRequiredPage() { + return ( + <> + <EmptyState> + <EmptyStateIcon icon={LockIcon} /> + <Title headingLevel="h1">Unauthorized</Title> + <EmptyStateBody> + <p> + Authorization is required. + </p> + </EmptyStateBody> + </EmptyState> + </> + ) +} + + +export default AuthRequiredPage diff --git a/web/src/reducers/configErrors.js b/web/src/reducers/configErrors.js index dd0a28423..5cab17c0b 100644 --- a/web/src/reducers/configErrors.js +++ b/web/src/reducers/configErrors.js @@ -12,10 +12,14 @@ // License for the specific language governing permissions and limitations // under the License. -export default (state = [], action) => { +export default (state = {errors: [], ready: false}, action) => { switch (action.type) { - case 'FETCH_CONFIGERRORS_SUCCESS': - return action.errors + case 'CONFIGERRORS_FETCH_SUCCESS': + return {errors: action.errors, ready: true} + case 'CONFIGERRORS_FETCH_FAIL': + return {errors: [], ready: true} + case 'CONFIGERRORS_CLEAR': + return {errors: [], ready: false} default: return state } diff --git a/web/src/reducers/notifications.js b/web/src/reducers/notifications.js index 71e0341db..1c8783751 100644 --- a/web/src/reducers/notifications.js +++ b/web/src/reducers/notifications.js @@ -22,6 +22,7 @@ import { export default (state = [], action) => { // Intercept API failure + // TODO: Are these still used? if (action.notification && action.type.match(/.*_FETCH_FAIL$/)) { action = addApiError(action.notification) } diff --git a/web/src/reducers/tenant.js b/web/src/reducers/tenant.js index 5911c834c..cbca4f11b 100644 --- a/web/src/reducers/tenant.js +++ b/web/src/reducers/tenant.js @@ -14,7 +14,9 @@ import { TENANT_SET } from '../actions/tenant' -export default (state = {name: null}, action) => { +// undefined name means we haven't loaded anything yet; null means +// outside of tenant context. +export default (state = {name: undefined}, action) => { switch (action.type) { case TENANT_SET: return action.tenant diff --git a/web/src/reducers/user.js b/web/src/reducers/user.js index 215cbfb1a..54b34cb19 100644 --- a/web/src/reducers/user.js +++ b/web/src/reducers/user.js @@ -28,7 +28,9 @@ export default (state = { data: null, scope: [], isAdmin: false, - tenant: null, + // undefined tenant means we haven't loaded anything yet; null means + // outside of tenant context. + tenant: undefined, redirect: null, }, action) => { switch (action.type) { @@ -39,7 +41,8 @@ export default (state = { token: action.token, redirect: action.redirect, scope: [], - isAdmin: false + isAdmin: false, + tenant: undefined, } } case USER_LOGGED_OUT: @@ -49,7 +52,8 @@ export default (state = { token: null, redirect: null, scope: [], - isAdmin: false + isAdmin: false, + tenant: undefined, } case USER_ACL_REQUEST: return { diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index e9cff38d1..1650b2f7d 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -804,6 +804,7 @@ class ZuulWebAPI(object): 'info': '/api/info', 'connections': '/api/connections', 'components': '/api/components', + 'authorizations': '/api/authorizations', 'tenants': '/api/tenants', 'tenant_info': '/api/tenant/{tenant}/info', 'status': '/api/tenant/{tenant}/status', @@ -897,7 +898,7 @@ class ZuulWebAPI(object): else: tenant_name = '*' admin_rules = [] - access_rules = self.zuulweb.api_root.access_rules + access_rules = self.zuulweb.abide.api_root.access_rules override = claims.get('zuul', {}).get('admin', []) if (override == '*' or (isinstance(override, list) and tenant_name in override)): @@ -955,6 +956,14 @@ class ZuulWebAPI(object): @cherrypy.expose @cherrypy.tools.json_out(content_type='application/json; charset=utf-8') @cherrypy.tools.handle_options() + @cherrypy.tools.check_root_auth(require_auth=True) + def root_authorizations(self, auth): + return {'zuul': {'admin': auth.admin, + 'scope': ['*']}, } + + @cherrypy.expose + @cherrypy.tools.json_out(content_type='application/json; charset=utf-8') + @cherrypy.tools.handle_options() @cherrypy.tools.check_tenant_auth(require_auth=True) def tenant_authorizations(self, tenant_name, tenant, auth): return {'zuul': {'admin': auth.admin, @@ -1950,6 +1959,10 @@ class ZuulWeb(object): '/api/tenant/{tenant_name}/authorizations', controller=api, action='tenant_authorizations') + route_map.connect('api', + '/api/authorizations', + controller=api, + action='root_authorizations') route_map.connect('api', '/api/tenant/{tenant_name}/promote', controller=api, action='promote') route_map.connect( |