-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/refresh #273
Feature/refresh #273
Changes from 4 commits
362cf06
096ed8f
248be52
cca5950
713fc1f
9b6c9a4
a0ee102
c77cfdc
91403ea
1498b60
71a4fe3
5e6193b
44a4e97
19c0de2
1b5a2df
f6b3136
0e8d792
09b84cd
d13f30a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,6 +65,64 @@ class ArboristClient { | |
}, | ||
); | ||
} | ||
|
||
listAuthorizedCreateResources(jwt) { | ||
// Make request to arborist for list of resources with access | ||
const resourcesEndpoint = `${this.baseEndpoint}/auth/mapping`; | ||
log.debug('[ArboristClient] listAuthorizedCreateResources jwt: ', jwt); | ||
|
||
const headers = (jwt) ? { Authorization: `bearer ${jwt}` } : {}; | ||
return fetch( | ||
resourcesEndpoint, | ||
{ | ||
method: 'POST', | ||
headers, | ||
}, | ||
).then( | ||
(response) => { | ||
if (response.status === 400) { | ||
// Retry with GET instead of POST. Older version of Arborist POST auth/mapping | ||
// didn't support token authentication. | ||
// This catch block can be removed in a little while, when it will likely not cause issues | ||
return fetch( | ||
resourcesEndpoint, | ||
{ | ||
method: 'GET', | ||
headers, | ||
}, | ||
).then((res) => res.json()); | ||
} | ||
return response.json(); | ||
}, | ||
(err) => { | ||
log.error(err); | ||
throw new CodedError(500, err); | ||
}, | ||
).then( | ||
(result) => { | ||
const data = { | ||
resources: [], | ||
}; | ||
Object.keys(result).forEach((key) => { | ||
// logic: you have access to a project if you have the following access: | ||
// method 'create' (or '*' - all methods) to service 'guppy' (or '*' - all services) | ||
// on the project resource. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is too broad, and it sort of binds the two roles of data submitter and Guppy admin togather. The restarting of Guppy should be an "admin-level" feature. I think we'd better to have a dedicated "guppy_admin" policy for this feature, which could be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see 713fc1f |
||
if (result[key] && result[key].some((x) => ( | ||
(x.method === 'create' || x.method === '*') | ||
&& (x.service === 'guppy' || x.service === '*') | ||
))) { | ||
data.resources.push(key); | ||
} | ||
}); | ||
log.debug('[ArboristClient] data: ', data); | ||
return data; | ||
}, | ||
(err) => { | ||
log.error(err); | ||
throw new CodedError(500, err); | ||
}, | ||
); | ||
} | ||
} | ||
|
||
export default new ArboristClient(config.arboristEndpoint); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,31 @@ export const getAccessibleResourcesFromArboristasync = async (jwt) => { | |
return resources; | ||
}; | ||
|
||
export const getAccessibleCreateResourcesFromArboristasync = async (jwt) => { | ||
let data; | ||
if (config.internalLocalTest) { | ||
data = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider fix the failed es-lint check here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
resources: [ // these are just for testing | ||
'/programs/DEV/projects/test', | ||
'/programs/jnkns/projects/jenkins', | ||
], | ||
}; | ||
} else { | ||
data = await arboristClient.listAuthorizedCreateResources(jwt); | ||
} | ||
|
||
log.debug('[authMiddleware] list create resources: ', JSON.stringify(data, null, 4)); | ||
if (data && data.error) { | ||
// if user is not in arborist db, assume has no access to any | ||
if (data.error.code === 404) { | ||
return []; | ||
} | ||
throw new CodedError(data.error.code, data.error.message); | ||
} | ||
const resources = data.resources ? _.uniq(data.resources) : []; | ||
return resources; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need to execute all those expensive array manipulations for this feature, if your goal is to determine whether am user is Guppy admin or not. This function can simply returns a boolean based on the return value from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok boolean is returned now when checking for refresh permissions |
||
}; | ||
|
||
export const getRequestResourceListFromFilter = async ( | ||
esIndex, | ||
esType, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,22 +19,50 @@ import downloadRouter from './download'; | |
import CodedError from './utils/error'; | ||
import { statusRouter, versionRouter } from './endpoints'; | ||
|
||
let server; | ||
const app = express(); | ||
app.use(cors()); | ||
app.use(helmet()); | ||
app.use(bodyParser.json({ limit: '50mb' })); | ||
|
||
const refreshRouter = async (req, res, next) => { | ||
res.setHeader('Content-Type', 'application/json; charset=utf-8'); | ||
try { | ||
if (config.allowRefresh) { | ||
log.debug('[Refresh] ', JSON.stringify(req.body, null, 4)); | ||
const jwt = headerParser.parseJWT(req); | ||
if (!jwt) { | ||
const noJwtError = new CodedError(401, '[Refresh] no JWT user token provided to _refresh function'); | ||
throw noJwtError; | ||
} | ||
const authHelper = await getAuthHelperInstance(jwt); | ||
console.log("AUTH HELPER: ", authHelper) | ||
if (authHelper._accessibleCreateResourceList === undefined || authHelper._accessibleCreateResourceList.length === 0) { | ||
const noPermsUser = new CodedError(401, '[Refresh] User cannot refresh Guppy without a valid token that has read access to at least one project'); | ||
throw noPermsUser; | ||
} | ||
await server.stop() | ||
await initializeAndStartServer(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This solution is generally ok, but it only works if there is only 1 guppy pod/container/server in the system. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mfshao thanks for all the feedback. In the case of replica guppy pods, is there a link you could provide for how requests from the clients are distributed to the guppy replicas? In other words, is there a nginx or helm configuration that fans out the requests? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mfshao Alternatively, as a brainstorm: guppy could checking mappings on a configurable interval and initiate a restart if the version changed. For example, for index
On startup, guppy could cache this value for each index, and then monitor the index for changes |
||
} else if (config.allowRefresh === false) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this check should happens first, before checking for user's token |
||
const disabledRefresh = new CodedError(404, '[Refresh] guppy _refresh functionality is not enabled'); | ||
throw disabledRefresh; | ||
} | ||
res.send("[Refresh] guppy refreshed successfully") | ||
} catch (err) { | ||
log.error(err); | ||
next(err); | ||
} | ||
return 0; | ||
}; | ||
|
||
const startServer = async () => { | ||
// build schema and resolvers by parsing elastic search fields and types, | ||
const typeDefs = getSchema(config.esConfig, esInstance); | ||
const resolvers = getResolver(config.esConfig, esInstance); | ||
const schema = makeExecutableSchema({ typeDefs, resolvers }); | ||
const schemaWithMiddleware = applyMiddleware( | ||
schema, | ||
...middlewares, | ||
); | ||
// create graphql server instance | ||
const server = new ApolloServer({ | ||
const schemaWithMiddleware = applyMiddleware(schema, ...middlewares); | ||
// create graphql server instance | ||
server = new ApolloServer({ | ||
mocks: false, | ||
schema: schemaWithMiddleware, | ||
validationRules: [depthLimit(10)], | ||
|
@@ -57,43 +85,49 @@ const startServer = async () => { | |
path: config.path, | ||
}), | ||
); | ||
log.info(`[Server] guppy listening on port ${config.port}!`); | ||
}; | ||
|
||
// simple health check endpoint | ||
// eslint-disable-next-line no-unused-vars | ||
app.get('/_status', statusRouter, (req, res, err, next) => { | ||
if (err instanceof CodedError) { | ||
// deepcode ignore ServerLeak: no important information exists in error | ||
res.status(err.code).send(err.msg); | ||
} else { | ||
// deepcode ignore ServerLeak: no important information exists in error | ||
res.status(500).send(err); | ||
} | ||
}); | ||
const initializeAndStartServer = async () => { | ||
await esInstance.initialize(); | ||
await startServer(); | ||
}; | ||
// simple health check endpoint | ||
// eslint-disable-next-line no-unused-vars | ||
app.get('/_status', statusRouter, (req, res, err, next) => { | ||
if (err instanceof CodedError) { | ||
// deepcode ignore ServerLeak: no important information exists in error | ||
res.status(err.code).send(err.msg); | ||
} else { | ||
// deepcode ignore ServerLeak: no important information exists in error | ||
res.status(500).send(err); | ||
} | ||
}); | ||
|
||
// eslint-disable-next-line no-unused-vars | ||
app.get('/_version', versionRouter); | ||
// eslint-disable-next-line no-unused-vars | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you don't need this for the next line There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
app.get('/_version', versionRouter); | ||
|
||
// download endpoint for fetching data directly from es | ||
app.post( | ||
'/download', | ||
downloadRouter, | ||
(err, req, res, next) => { // eslint-disable-line no-unused-vars | ||
if (err instanceof CodedError) { | ||
// deepcode ignore ServerLeak: no important information exists in error | ||
res.status(err.code).send(err.msg); | ||
} else { | ||
// deepcode ignore ServerLeak: no important information exists in error | ||
res.status(500).send(err); | ||
} | ||
}, | ||
); | ||
// download endpoint for fetching data directly from es | ||
app.post('/download', downloadRouter, (err, req, res, next) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. put There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
// eslint-disable-line no-unused-vars | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you don't need this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
if (err instanceof CodedError) { | ||
// deepcode ignore ServerLeak: no important information exists in error | ||
res.status(err.code).send(err.msg); | ||
} else { | ||
// deepcode ignore ServerLeak: no important information exists in error | ||
res.status(500).send(err); | ||
} | ||
}); | ||
|
||
app.listen(config.port, () => { | ||
log.info(`[Server] guppy listening on port ${config.port}!`); | ||
}); | ||
}; | ||
app.post('/_refresh',refreshRouter, (err, req, res, next) => { | ||
if (err instanceof CodedError) { | ||
res.status(err.code).send(err.msg); | ||
}else{ | ||
res.status(500).send(err) | ||
} | ||
}); | ||
|
||
// need to connect to ES and initialize before setting up a server | ||
esInstance.initialize().then(() => { | ||
startServer(); | ||
app.listen(config.port, async () => { | ||
await initializeAndStartServer(); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of this function is duplicating
listAuthorizedResources(jwt)
, except for the part that check's the user for certain service + methods.We don't need to duplicate those codes, we should be extracting the part of making request to arborist and checking for user's access in the returned auth mapping into two separated functions, and just reuse them. In this way, we can avoid sending duplicated requests to arborist
See how the fetch for user's auth mapping and check for user's access can be separated in another service
https://github.com/uc-cdis/data-portal/blob/master/src/actions.js#L513
https://github.com/uc-cdis/data-portal/blob/master/src/authMappingUtils.js#L53
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok getAuthMapping function has been created and used twice, once for the reader permissions checking, and again for the guppy admin permissions checking. See 713fc1f