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/connections',
|
||||||
'/api/components',
|
'/api/components',
|
||||||
'/api/tenants',
|
'/api/tenants',
|
||||||
|
'/api/authorizations',
|
||||||
'/api/tenant/{tenant}/status',
|
'/api/tenant/{tenant}/status',
|
||||||
'/api/tenant/{tenant}/status/change/{change}',
|
'/api/tenant/{tenant}/status/change/{change}',
|
||||||
'/api/tenant/{tenant}/jobs',
|
'/api/tenant/{tenant}/jobs',
|
||||||
|
|
|
@ -20,6 +20,7 @@ import PropTypes from 'prop-types'
|
||||||
import { matchPath, withRouter } from 'react-router'
|
import { matchPath, withRouter } from 'react-router'
|
||||||
import { Link, NavLink, Redirect, Route, Switch } from 'react-router-dom'
|
import { Link, NavLink, Redirect, Route, Switch } from 'react-router-dom'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
import { withAuth } from 'oidc-react'
|
||||||
import {
|
import {
|
||||||
TimedToastNotification,
|
TimedToastNotification,
|
||||||
ToastNotificationList,
|
ToastNotificationList,
|
||||||
|
@ -65,17 +66,19 @@ import SelectTz from './containers/timezone/SelectTz'
|
||||||
import ConfigModal from './containers/config/Config'
|
import ConfigModal from './containers/config/Config'
|
||||||
import logo from './images/logo.svg'
|
import logo from './images/logo.svg'
|
||||||
import { clearNotification } from './actions/notifications'
|
import { clearNotification } from './actions/notifications'
|
||||||
import { fetchConfigErrorsAction } from './actions/configErrors'
|
import { fetchConfigErrorsAction, clearConfigErrorsAction } from './actions/configErrors'
|
||||||
import { routes } from './routes'
|
import { routes } from './routes'
|
||||||
import { setTenantAction } from './actions/tenant'
|
import { setTenantAction } from './actions/tenant'
|
||||||
import { configureAuthFromTenant, configureAuthFromInfo } from './actions/auth'
|
import { configureAuthFromTenant, configureAuthFromInfo } from './actions/auth'
|
||||||
import { getHomepageUrl } from './api'
|
import { getHomepageUrl } from './api'
|
||||||
import AuthCallbackPage from './pages/AuthCallback'
|
import AuthCallbackPage from './pages/AuthCallback'
|
||||||
|
import AuthRequiredPage from './pages/AuthRequired'
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
notifications: PropTypes.array,
|
notifications: PropTypes.array,
|
||||||
configErrors: PropTypes.array,
|
configErrors: PropTypes.array,
|
||||||
|
configErrorsReady: PropTypes.bool,
|
||||||
info: PropTypes.object,
|
info: PropTypes.object,
|
||||||
tenant: PropTypes.object,
|
tenant: PropTypes.object,
|
||||||
timezone: PropTypes.string,
|
timezone: PropTypes.string,
|
||||||
|
@ -85,6 +88,7 @@ class App extends React.Component {
|
||||||
isKebabDropdownOpen: PropTypes.bool,
|
isKebabDropdownOpen: PropTypes.bool,
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
auth: PropTypes.object,
|
auth: PropTypes.object,
|
||||||
|
signIn: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
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 = () => {
|
renderContent = () => {
|
||||||
const { info, tenant, auth } = this.props
|
const { tenant, auth, user } = this.props
|
||||||
const allRoutes = []
|
const allRoutes = []
|
||||||
|
|
||||||
if ((window.location.origin + window.location.pathname) ===
|
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)
|
// validation is complete (it will internally redirect when complete)
|
||||||
return <AuthCallbackPage/>
|
return <AuthCallbackPage/>
|
||||||
}
|
}
|
||||||
if (info.isFetching || !auth.info || auth.isFetching) {
|
if (!this.isAuthReady()) {
|
||||||
return <Fetching />
|
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
|
this.menu
|
||||||
// Do not include '/tenants' route in white-label setup
|
// Do not include '/tenants' route in white-label setup
|
||||||
.filter(item =>
|
.filter(item =>
|
||||||
|
@ -164,9 +187,10 @@ class App extends React.Component {
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
// This method is called when info property is updated
|
// 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) {
|
if (info.ready) {
|
||||||
let tenantName, whiteLabel
|
let tenantName = null
|
||||||
|
let whiteLabel
|
||||||
|
|
||||||
if (info.tenant) {
|
if (info.tenant) {
|
||||||
// White label
|
// White label
|
||||||
|
@ -188,7 +212,7 @@ class App extends React.Component {
|
||||||
const tenantAction = setTenantAction(tenantName, whiteLabel)
|
const tenantAction = setTenantAction(tenantName, whiteLabel)
|
||||||
this.props.dispatch(tenantAction)
|
this.props.dispatch(tenantAction)
|
||||||
if (tenantName) {
|
if (tenantName) {
|
||||||
this.props.dispatch(fetchConfigErrorsAction(tenantAction.tenant))
|
this.props.dispatch(clearConfigErrorsAction())
|
||||||
}
|
}
|
||||||
if (whiteLabel || !tenantName) {
|
if (whiteLabel || !tenantName) {
|
||||||
// The app info endpoint was already a tenant info
|
// The app info endpoint was already a tenant info
|
||||||
|
@ -199,6 +223,12 @@ class App extends React.Component {
|
||||||
this.props.dispatch(configureAuthFromTenant(tenantName))
|
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(
|
export default withRouter(connect(
|
||||||
state => ({
|
state => ({
|
||||||
notifications: state.notifications,
|
notifications: state.notifications,
|
||||||
configErrors: state.configErrors,
|
configErrors: state.configErrors.errors,
|
||||||
|
configErrorsReady: state.configErrors.ready,
|
||||||
info: state.info,
|
info: state.info,
|
||||||
tenant: state.tenant,
|
tenant: state.tenant,
|
||||||
timezone: state.timezone,
|
timezone: state.timezone,
|
||||||
user: state.user,
|
user: state.user,
|
||||||
auth: state.auth,
|
auth: state.auth,
|
||||||
})
|
})
|
||||||
)(App))
|
)(withAuth(App)))
|
||||||
|
|
|
@ -18,11 +18,19 @@ export function fetchConfigErrorsAction (tenant) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
return fetchConfigErrors(tenant.apiPrefix)
|
return fetchConfigErrors(tenant.apiPrefix)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
dispatch({type: 'FETCH_CONFIGERRORS_SUCCESS',
|
dispatch({type: 'CONFIGERRORS_FETCH_SUCCESS',
|
||||||
errors: response.data})
|
errors: response.data})
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.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) => {
|
export const userLoggedIn = (user, redirect) => (dispatch) => {
|
||||||
|
const token = getToken(user)
|
||||||
|
API.setAuthToken(token)
|
||||||
dispatch({
|
dispatch({
|
||||||
type: USER_LOGGED_IN,
|
type: USER_LOGGED_IN,
|
||||||
user: user,
|
user: user,
|
||||||
token: getToken(user),
|
token: token,
|
||||||
redirect: redirect,
|
redirect: redirect,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -62,10 +64,10 @@ const fetchUserACLFail = error => ({
|
||||||
error
|
error
|
||||||
})
|
})
|
||||||
|
|
||||||
export const fetchUserACL = (tenant, user) => (dispatch) => {
|
export const fetchUserACL = (tenant) => (dispatch) => {
|
||||||
dispatch(fetchUserACLRequest(tenant))
|
dispatch(fetchUserACLRequest(tenant))
|
||||||
let apiPrefix = 'tenant/' + tenant + '/'
|
let apiPrefix = tenant? 'tenant/' + tenant + '/' : ''
|
||||||
return API.fetchUserAuthorizations(apiPrefix, user.token)
|
return API.fetchUserAuthorizations(apiPrefix)
|
||||||
.then(response => dispatch(fetchUserACLSuccess(response.data)))
|
.then(response => dispatch(fetchUserACLSuccess(response.data)))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
dispatch(fetchUserACLFail(error))
|
dispatch(fetchUserACLFail(error))
|
||||||
|
|
179
web/src/api.js
179
web/src/api.js
|
@ -14,6 +14,12 @@
|
||||||
|
|
||||||
import Axios from 'axios'
|
import Axios from 'axios'
|
||||||
|
|
||||||
|
let authToken = undefined
|
||||||
|
|
||||||
|
export function setAuthToken(token) {
|
||||||
|
authToken = token
|
||||||
|
}
|
||||||
|
|
||||||
function getHomepageUrl(url) {
|
function getHomepageUrl(url) {
|
||||||
//
|
//
|
||||||
// Discover serving location from href.
|
// Discover serving location from href.
|
||||||
|
@ -103,14 +109,25 @@ function getStreamUrl(apiPrefix) {
|
||||||
return streamUrl
|
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
|
// This performs a simple GET and tries to detect if CORS errors are
|
||||||
// due to proxy authentication errors.
|
// due to proxy authentication errors.
|
||||||
const instance = Axios.create({
|
const instance = Axios.create({
|
||||||
baseURL: apiUrl
|
baseURL: apiUrl
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (authToken) {
|
||||||
|
instance.defaults.headers.common['Authorization'] = 'Bearer ' + authToken
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {method, url, data}
|
||||||
|
|
||||||
// First try the request as normal
|
// First try the request as normal
|
||||||
let res = instance.get(url).catch(err => {
|
let res = instance.request(config).catch(err => {
|
||||||
if (err.response === undefined) {
|
if (err.response === undefined) {
|
||||||
// This is either a Network, DNS, or CORS error, but we can't tell which.
|
// 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
|
// 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
|
// 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.
|
// will instead issue a 403. We can use this to detect that case.
|
||||||
instance.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
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) {
|
if (err2.response && err2.response.status === 403) {
|
||||||
// We might be getting a redirect or something else,
|
// We might be getting a redirect or something else,
|
||||||
// so reload the page.
|
// so reload the page.
|
||||||
|
@ -133,165 +150,165 @@ function getWithCorsHandling(url) {
|
||||||
})
|
})
|
||||||
return res2
|
return res2
|
||||||
}
|
}
|
||||||
|
throw (err)
|
||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct APIs
|
// Direct APIs
|
||||||
function fetchInfo() {
|
function fetchInfo() {
|
||||||
return getWithCorsHandling('info')
|
return makeRequest('info')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchComponents() {
|
function fetchComponents() {
|
||||||
return getWithCorsHandling('components')
|
return makeRequest('components')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchTenantInfo(apiPrefix) {
|
function fetchTenantInfo(apiPrefix) {
|
||||||
return getWithCorsHandling(apiPrefix + 'info')
|
return makeRequest(apiPrefix + 'info')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchOpenApi() {
|
function fetchOpenApi() {
|
||||||
return Axios.get(getHomepageUrl() + 'openapi.yaml')
|
return Axios.get(getHomepageUrl() + 'openapi.yaml')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchTenants() {
|
function fetchTenants() {
|
||||||
return getWithCorsHandling(apiUrl + 'tenants')
|
return makeRequest(apiUrl + 'tenants')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchConfigErrors(apiPrefix) {
|
function fetchConfigErrors(apiPrefix) {
|
||||||
return getWithCorsHandling(apiPrefix + 'config-errors')
|
return makeRequest(apiPrefix + 'config-errors')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchStatus(apiPrefix) {
|
function fetchStatus(apiPrefix) {
|
||||||
return getWithCorsHandling(apiPrefix + 'status')
|
return makeRequest(apiPrefix + 'status')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchChangeStatus(apiPrefix, changeId) {
|
function fetchChangeStatus(apiPrefix, changeId) {
|
||||||
return getWithCorsHandling(apiPrefix + 'status/change/' + changeId)
|
return makeRequest(apiPrefix + 'status/change/' + changeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchFreezeJob(apiPrefix, pipelineName, projectName, branchName, jobName) {
|
function fetchFreezeJob(apiPrefix, pipelineName, projectName, branchName, jobName) {
|
||||||
return getWithCorsHandling(apiPrefix +
|
return makeRequest(apiPrefix +
|
||||||
'pipeline/' + pipelineName +
|
'pipeline/' + pipelineName +
|
||||||
'/project/' + projectName +
|
'/project/' + projectName +
|
||||||
'/branch/' + branchName +
|
'/branch/' + branchName +
|
||||||
'/freeze-job/' + jobName)
|
'/freeze-job/' + jobName)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchBuild(apiPrefix, buildId) {
|
function fetchBuild(apiPrefix, buildId) {
|
||||||
return getWithCorsHandling(apiPrefix + 'build/' + buildId)
|
return makeRequest(apiPrefix + 'build/' + buildId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchBuilds(apiPrefix, queryString) {
|
function fetchBuilds(apiPrefix, queryString) {
|
||||||
let path = 'builds'
|
let path = 'builds'
|
||||||
if (queryString) {
|
if (queryString) {
|
||||||
path += '?' + queryString.slice(1)
|
path += '?' + queryString.slice(1)
|
||||||
}
|
}
|
||||||
return getWithCorsHandling(apiPrefix + path)
|
return makeRequest(apiPrefix + path)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchBuildset(apiPrefix, buildsetId) {
|
function fetchBuildset(apiPrefix, buildsetId) {
|
||||||
return getWithCorsHandling(apiPrefix + 'buildset/' + buildsetId)
|
return makeRequest(apiPrefix + 'buildset/' + buildsetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchBuildsets(apiPrefix, queryString) {
|
function fetchBuildsets(apiPrefix, queryString) {
|
||||||
let path = 'buildsets'
|
let path = 'buildsets'
|
||||||
if (queryString) {
|
if (queryString) {
|
||||||
path += '?' + queryString.slice(1)
|
path += '?' + queryString.slice(1)
|
||||||
}
|
}
|
||||||
return getWithCorsHandling(apiPrefix + path)
|
return makeRequest(apiPrefix + path)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchPipelines(apiPrefix) {
|
function fetchPipelines(apiPrefix) {
|
||||||
return getWithCorsHandling(apiPrefix + 'pipelines')
|
return makeRequest(apiPrefix + 'pipelines')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchProject(apiPrefix, projectName) {
|
function fetchProject(apiPrefix, projectName) {
|
||||||
return getWithCorsHandling(apiPrefix + 'project/' + projectName)
|
return makeRequest(apiPrefix + 'project/' + projectName)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchProjects(apiPrefix) {
|
function fetchProjects(apiPrefix) {
|
||||||
return getWithCorsHandling(apiPrefix + 'projects')
|
return makeRequest(apiPrefix + 'projects')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchJob(apiPrefix, jobName) {
|
function fetchJob(apiPrefix, jobName) {
|
||||||
return getWithCorsHandling(apiPrefix + 'job/' + jobName)
|
return makeRequest(apiPrefix + 'job/' + jobName)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchJobGraph(apiPrefix, projectName, pipelineName, branchName) {
|
function fetchJobGraph(apiPrefix, projectName, pipelineName, branchName) {
|
||||||
return getWithCorsHandling(apiPrefix +
|
return makeRequest(apiPrefix +
|
||||||
'pipeline/' + pipelineName +
|
'pipeline/' + pipelineName +
|
||||||
'/project/' + projectName +
|
'/project/' + projectName +
|
||||||
'/branch/' + branchName +
|
'/branch/' + branchName +
|
||||||
'/freeze-jobs')
|
'/freeze-jobs')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchJobs(apiPrefix) {
|
function fetchJobs(apiPrefix) {
|
||||||
return getWithCorsHandling(apiPrefix + 'jobs')
|
return makeRequest(apiPrefix + 'jobs')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchLabels(apiPrefix) {
|
function fetchLabels(apiPrefix) {
|
||||||
return getWithCorsHandling(apiPrefix + 'labels')
|
return makeRequest(apiPrefix + 'labels')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchNodes(apiPrefix) {
|
function fetchNodes(apiPrefix) {
|
||||||
return getWithCorsHandling(apiPrefix + 'nodes')
|
return makeRequest(apiPrefix + 'nodes')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchSemaphores(apiPrefix) {
|
function fetchSemaphores(apiPrefix) {
|
||||||
return Axios.get(apiUrl + apiPrefix + 'semaphores')
|
return makeRequest(apiPrefix + 'semaphores')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchAutoholds(apiPrefix) {
|
function fetchAutoholds(apiPrefix) {
|
||||||
return getWithCorsHandling(apiPrefix + 'autohold')
|
return makeRequest(apiPrefix + 'autohold')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchAutohold(apiPrefix, requestId) {
|
function fetchAutohold(apiPrefix, requestId) {
|
||||||
return getWithCorsHandling(apiPrefix + 'autohold/' + requestId)
|
return makeRequest(apiPrefix + 'autohold/' + requestId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// token-protected API
|
function fetchUserAuthorizations(apiPrefix) {
|
||||||
function fetchUserAuthorizations(apiPrefix, token) {
|
return makeRequest(apiPrefix + 'authorizations')
|
||||||
// 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 dequeue(apiPrefix, projectName, pipeline, change, token) {
|
function dequeue(apiPrefix, projectName, pipeline, change) {
|
||||||
const instance = Axios.create({
|
return makeRequest(
|
||||||
baseURL: apiUrl
|
|
||||||
})
|
|
||||||
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token
|
|
||||||
let res = instance.post(
|
|
||||||
apiPrefix + 'project/' + projectName + '/dequeue',
|
apiPrefix + 'project/' + projectName + '/dequeue',
|
||||||
|
'post',
|
||||||
{
|
{
|
||||||
pipeline: pipeline,
|
pipeline: pipeline,
|
||||||
change: change,
|
change: change,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
function dequeue_ref(apiPrefix, projectName, pipeline, ref, token) {
|
|
||||||
const instance = Axios.create({
|
function dequeue_ref(apiPrefix, projectName, pipeline, ref) {
|
||||||
baseURL: apiUrl
|
return makeRequest(
|
||||||
})
|
|
||||||
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token
|
|
||||||
let res = instance.post(
|
|
||||||
apiPrefix + 'project/' + projectName + '/dequeue',
|
apiPrefix + 'project/' + projectName + '/dequeue',
|
||||||
|
'post',
|
||||||
{
|
{
|
||||||
pipeline: pipeline,
|
pipeline: pipeline,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function enqueue(apiPrefix, projectName, pipeline, change, token) {
|
function enqueue(apiPrefix, projectName, pipeline, change) {
|
||||||
const instance = Axios.create({
|
return makeRequest(
|
||||||
baseURL: apiUrl
|
|
||||||
})
|
|
||||||
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token
|
|
||||||
let res = instance.post(
|
|
||||||
apiPrefix + 'project/' + projectName + '/enqueue',
|
apiPrefix + 'project/' + projectName + '/enqueue',
|
||||||
|
'post',
|
||||||
{
|
{
|
||||||
pipeline: pipeline,
|
pipeline: pipeline,
|
||||||
change: change,
|
change: change,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
function enqueue_ref(apiPrefix, projectName, pipeline, ref, oldrev, newrev, token) {
|
|
||||||
const instance = Axios.create({
|
function enqueue_ref(apiPrefix, projectName, pipeline, ref, oldrev, newrev) {
|
||||||
baseURL: apiUrl
|
return makeRequest(
|
||||||
})
|
|
||||||
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token
|
|
||||||
let res = instance.post(
|
|
||||||
apiPrefix + 'project/' + projectName + '/enqueue',
|
apiPrefix + 'project/' + projectName + '/enqueue',
|
||||||
|
'post',
|
||||||
{
|
{
|
||||||
pipeline: pipeline,
|
pipeline: pipeline,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
|
@ -299,16 +316,13 @@ function enqueue_ref(apiPrefix, projectName, pipeline, ref, oldrev, newrev, toke
|
||||||
newrev: newrev,
|
newrev: newrev,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function autohold(apiPrefix, projectName, job, change, ref,
|
function autohold(apiPrefix, projectName, job, change, ref,
|
||||||
reason, count, node_hold_expiration, token) {
|
reason, count, node_hold_expiration) {
|
||||||
const instance = Axios.create({
|
return makeRequest(
|
||||||
baseURL: apiUrl
|
|
||||||
})
|
|
||||||
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token
|
|
||||||
let res = instance.post(
|
|
||||||
apiPrefix + 'project/' + projectName + '/autohold',
|
apiPrefix + 'project/' + projectName + '/autohold',
|
||||||
|
'post',
|
||||||
{
|
{
|
||||||
change: change,
|
change: change,
|
||||||
job: job,
|
job: job,
|
||||||
|
@ -318,33 +332,24 @@ function autohold(apiPrefix, projectName, job, change, ref,
|
||||||
node_hold_expiration: node_hold_expiration,
|
node_hold_expiration: node_hold_expiration,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function autohold_delete(apiPrefix, requestId, token) {
|
function autohold_delete(apiPrefix, requestId) {
|
||||||
const instance = Axios.create({
|
return makeRequest(
|
||||||
baseURL: apiUrl
|
apiPrefix + '/autohold/' + requestId,
|
||||||
})
|
'delete'
|
||||||
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token
|
|
||||||
let res = instance.delete(
|
|
||||||
apiPrefix + '/autohold/' + requestId
|
|
||||||
)
|
)
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function promote(apiPrefix, pipeline, changes, token) {
|
function promote(apiPrefix, pipeline, changes) {
|
||||||
const instance = Axios.create({
|
return makeRequest(
|
||||||
baseURL: apiUrl
|
|
||||||
})
|
|
||||||
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token
|
|
||||||
let res = instance.post(
|
|
||||||
apiPrefix + '/promote',
|
apiPrefix + '/promote',
|
||||||
|
'post',
|
||||||
{
|
{
|
||||||
pipeline: pipeline,
|
pipeline: pipeline,
|
||||||
changes: changes,
|
changes: changes,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -104,8 +104,14 @@ class AuthContainer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const { user, tenant } = this.props
|
||||||
this.props.userManager.events.addAccessTokenExpired(this.onAccessTokenExpired)
|
this.props.userManager.events.addAccessTokenExpired(this.onAccessTokenExpired)
|
||||||
this.props.userManager.events.addUserLoaded(this.onUserLoaded)
|
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() {
|
componentWillUnmount() {
|
||||||
|
@ -119,7 +125,7 @@ class AuthContainer extends React.Component {
|
||||||
// Make sure the token is current and the tenant is up to date.
|
// Make sure the token is current and the tenant is up to date.
|
||||||
if (user.data && user.tenant !== tenant.name) {
|
if (user.data && user.tenant !== tenant.name) {
|
||||||
console.log('Refreshing ACL', 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) {
|
function handleAutoholdDelete(requestId) {
|
||||||
autohold_delete(tenant.apiPrefix, requestId, user.token)
|
autohold_delete(tenant.apiPrefix, requestId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(addNotification(
|
dispatch(addNotification(
|
||||||
{
|
{
|
||||||
|
|
|
@ -64,7 +64,7 @@ const AutoholdModal = props => {
|
||||||
let ah_change = change === '' ? null : change
|
let ah_change = change === '' ? null : change
|
||||||
let ah_ref = changeRef === '' ? null : changeRef
|
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(() => {
|
.then(() => {
|
||||||
/* TODO it looks like there is a delay in the registering of the autohold request
|
/* 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
|
by the backend, meaning we sometimes do not get the newly created request after
|
||||||
|
@ -209,4 +209,4 @@ AutoholdModal.propTypes = {
|
||||||
export default connect((state) => ({
|
export default connect((state) => ({
|
||||||
tenant: state.tenant,
|
tenant: state.tenant,
|
||||||
user: state.user,
|
user: state.user,
|
||||||
}))(AutoholdModal)
|
}))(AutoholdModal)
|
||||||
|
|
|
@ -181,7 +181,7 @@ function Buildset({ buildset, timezone, tenant, user }) {
|
||||||
if (buildset.change === null) {
|
if (buildset.change === null) {
|
||||||
const oldrev = '0000000000000000000000000000000000000000'
|
const oldrev = '0000000000000000000000000000000000000000'
|
||||||
const newrev = buildset.newrev ? buildset.newrev : '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(() => {
|
.then(() => {
|
||||||
dispatch(addNotification(
|
dispatch(addNotification(
|
||||||
{
|
{
|
||||||
|
@ -196,7 +196,7 @@ function Buildset({ buildset, timezone, tenant, user }) {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const changeId = buildset.change + ',' + buildset.patchset
|
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(() => {
|
.then(() => {
|
||||||
dispatch(addNotification(
|
dispatch(addNotification(
|
||||||
{
|
{
|
||||||
|
|
|
@ -58,14 +58,14 @@ class Change extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
dequeueConfirm = () => {
|
dequeueConfirm = () => {
|
||||||
const { tenant, user, change, pipeline } = this.props
|
const { tenant, change, pipeline } = this.props
|
||||||
let projectName = change.project
|
let projectName = change.project
|
||||||
let changeId = change.id || 'N/A'
|
let changeId = change.id || 'N/A'
|
||||||
let changeRef = change.ref
|
let changeRef = change.ref
|
||||||
this.setState(() => ({ showDequeueModal: false }))
|
this.setState(() => ({ showDequeueModal: false }))
|
||||||
// post-merge
|
// post-merge
|
||||||
if (/^[0-9a-f]{40}$/.test(changeId)) {
|
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(() => {
|
.then(() => {
|
||||||
this.props.dispatch(fetchStatusIfNeeded(tenant))
|
this.props.dispatch(fetchStatusIfNeeded(tenant))
|
||||||
})
|
})
|
||||||
|
@ -74,7 +74,7 @@ class Change extends React.Component {
|
||||||
})
|
})
|
||||||
// pre-merge, ie we have a change id
|
// pre-merge, ie we have a change id
|
||||||
} else if (changeId !== 'N/A') {
|
} else if (changeId !== 'N/A') {
|
||||||
dequeue(tenant.apiPrefix, projectName, pipeline.name, changeId, user.token)
|
dequeue(tenant.apiPrefix, projectName, pipeline.name, changeId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.props.dispatch(fetchStatusIfNeeded(tenant))
|
this.props.dispatch(fetchStatusIfNeeded(tenant))
|
||||||
})
|
})
|
||||||
|
@ -118,11 +118,11 @@ class Change extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
promoteConfirm = () => {
|
promoteConfirm = () => {
|
||||||
const { tenant, user, change, pipeline } = this.props
|
const { tenant, change, pipeline } = this.props
|
||||||
let changeId = change.id || 'NA'
|
let changeId = change.id || 'NA'
|
||||||
this.setState(() => ({ showPromoteModal: false }))
|
this.setState(() => ({ showPromoteModal: false }))
|
||||||
if (changeId !== 'N/A') {
|
if (changeId !== 'N/A') {
|
||||||
promote(tenant.apiPrefix, pipeline.name, [changeId,], user.token)
|
promote(tenant.apiPrefix, pipeline.name, [changeId,])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.props.dispatch(fetchStatusIfNeeded(tenant))
|
this.props.dispatch(fetchStatusIfNeeded(tenant))
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// Copyright 2020 Red Hat, Inc
|
// Copyright 2020 Red Hat, Inc
|
||||||
|
// Copyright 2022 Acme Gating, LLC
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
// 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
|
// 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
|
// License for the specific language governing permissions and limitations
|
||||||
// under the License.
|
// under the License.
|
||||||
|
|
||||||
export default (state = [], action) => {
|
export default (state = {errors: [], ready: false}, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'FETCH_CONFIGERRORS_SUCCESS':
|
case 'CONFIGERRORS_FETCH_SUCCESS':
|
||||||
return action.errors
|
return {errors: action.errors, ready: true}
|
||||||
|
case 'CONFIGERRORS_FETCH_FAIL':
|
||||||
|
return {errors: [], ready: true}
|
||||||
|
case 'CONFIGERRORS_CLEAR':
|
||||||
|
return {errors: [], ready: false}
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
|
|
||||||
export default (state = [], action) => {
|
export default (state = [], action) => {
|
||||||
// Intercept API failure
|
// Intercept API failure
|
||||||
|
// TODO: Are these still used?
|
||||||
if (action.notification && action.type.match(/.*_FETCH_FAIL$/)) {
|
if (action.notification && action.type.match(/.*_FETCH_FAIL$/)) {
|
||||||
action = addApiError(action.notification)
|
action = addApiError(action.notification)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,9 @@
|
||||||
|
|
||||||
import { TENANT_SET } from '../actions/tenant'
|
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) {
|
switch (action.type) {
|
||||||
case TENANT_SET:
|
case TENANT_SET:
|
||||||
return action.tenant
|
return action.tenant
|
||||||
|
|
|
@ -28,7 +28,9 @@ export default (state = {
|
||||||
data: null,
|
data: null,
|
||||||
scope: [],
|
scope: [],
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
tenant: null,
|
// undefined tenant means we haven't loaded anything yet; null means
|
||||||
|
// outside of tenant context.
|
||||||
|
tenant: undefined,
|
||||||
redirect: null,
|
redirect: null,
|
||||||
}, action) => {
|
}, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
@ -39,7 +41,8 @@ export default (state = {
|
||||||
token: action.token,
|
token: action.token,
|
||||||
redirect: action.redirect,
|
redirect: action.redirect,
|
||||||
scope: [],
|
scope: [],
|
||||||
isAdmin: false
|
isAdmin: false,
|
||||||
|
tenant: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case USER_LOGGED_OUT:
|
case USER_LOGGED_OUT:
|
||||||
|
@ -49,7 +52,8 @@ export default (state = {
|
||||||
token: null,
|
token: null,
|
||||||
redirect: null,
|
redirect: null,
|
||||||
scope: [],
|
scope: [],
|
||||||
isAdmin: false
|
isAdmin: false,
|
||||||
|
tenant: undefined,
|
||||||
}
|
}
|
||||||
case USER_ACL_REQUEST:
|
case USER_ACL_REQUEST:
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -804,6 +804,7 @@ class ZuulWebAPI(object):
|
||||||
'info': '/api/info',
|
'info': '/api/info',
|
||||||
'connections': '/api/connections',
|
'connections': '/api/connections',
|
||||||
'components': '/api/components',
|
'components': '/api/components',
|
||||||
|
'authorizations': '/api/authorizations',
|
||||||
'tenants': '/api/tenants',
|
'tenants': '/api/tenants',
|
||||||
'tenant_info': '/api/tenant/{tenant}/info',
|
'tenant_info': '/api/tenant/{tenant}/info',
|
||||||
'status': '/api/tenant/{tenant}/status',
|
'status': '/api/tenant/{tenant}/status',
|
||||||
|
@ -897,7 +898,7 @@ class ZuulWebAPI(object):
|
||||||
else:
|
else:
|
||||||
tenant_name = '*'
|
tenant_name = '*'
|
||||||
admin_rules = []
|
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', [])
|
override = claims.get('zuul', {}).get('admin', [])
|
||||||
if (override == '*' or
|
if (override == '*' or
|
||||||
(isinstance(override, list) and tenant_name in override)):
|
(isinstance(override, list) and tenant_name in override)):
|
||||||
|
@ -952,6 +953,14 @@ class ZuulWebAPI(object):
|
||||||
break
|
break
|
||||||
return (access, admin)
|
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.expose
|
||||||
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
|
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
|
||||||
@cherrypy.tools.handle_options()
|
@cherrypy.tools.handle_options()
|
||||||
|
@ -1950,6 +1959,10 @@ class ZuulWeb(object):
|
||||||
'/api/tenant/{tenant_name}/authorizations',
|
'/api/tenant/{tenant_name}/authorizations',
|
||||||
controller=api,
|
controller=api,
|
||||||
action='tenant_authorizations')
|
action='tenant_authorizations')
|
||||||
|
route_map.connect('api',
|
||||||
|
'/api/authorizations',
|
||||||
|
controller=api,
|
||||||
|
action='root_authorizations')
|
||||||
route_map.connect('api', '/api/tenant/{tenant_name}/promote',
|
route_map.connect('api', '/api/tenant/{tenant_name}/promote',
|
||||||
controller=api, action='promote')
|
controller=api, action='promote')
|
||||||
route_map.connect(
|
route_map.connect(
|
||||||
|
|
Loading…
Reference in New Issue