summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tests/unit/test_web.py1
-rw-r--r--web/src/App.jsx47
-rw-r--r--web/src/actions/configErrors.js12
-rw-r--r--web/src/actions/user.js10
-rw-r--r--web/src/api.js179
-rw-r--r--web/src/containers/auth/Auth.jsx8
-rw-r--r--web/src/containers/autohold/AutoholdTable.jsx2
-rw-r--r--web/src/containers/autohold/autoholdModal.jsx4
-rw-r--r--web/src/containers/build/Buildset.jsx4
-rw-r--r--web/src/containers/status/Change.jsx10
-rw-r--r--web/src/pages/AuthCallback.jsx1
-rw-r--r--web/src/pages/AuthRequired.jsx44
-rw-r--r--web/src/reducers/configErrors.js10
-rw-r--r--web/src/reducers/notifications.js1
-rw-r--r--web/src/reducers/tenant.js4
-rw-r--r--web/src/reducers/user.js10
-rwxr-xr-xzuul/web/__init__.py15
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(