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
This commit is contained in:
parent
95ec2c45e5
commit
9d2e1339ff
|
@ -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',
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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'})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
179
web/src/api.js
179
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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
}))(AutoholdModal)
|
||||
|
|
|
@ -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(
|
||||
{
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)):
|
||||
|
@ -952,6 +953,14 @@ class ZuulWebAPI(object):
|
|||
break
|
||||
return (access, admin)
|
||||
|
||||
@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()
|
||||
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue