summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames E. Blair <jim@acmegating.com>2022-09-30 11:05:46 -0700
committerJames E. Blair <jim@acmegating.com>2022-10-25 20:22:42 -0700
commit9d2e1339ff9f5080cd23e9d29fcb08315a32e5e9 (patch)
treec3f4065df16f8d0ab608c7db80a800900e993348
parent95ec2c45e5cf0369f97aebe33e093698048a3fdb (diff)
downloadzuul-9d2e1339ff9f5080cd23e9d29fcb08315a32e5e9.tar.gz
Support authz for read-only web access
This updates the web UI to support the requirement for authn/z for read-only access. If authz is required for read access, we will automatically redirect. If we return and still aren't authorized, we will display an "Authorization required" page (rather than continuing and popping up API error notifications). The API methods are updated to send an authorization token whenever one is present. Change-Id: I31c13c943d05819b4122fcbcf2eaf41515c5b1d9
-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(