feat(xo-server): first endpoints of the beta REST API

This commit is contained in:
Julien Fontanet
2022-01-06 10:28:15 +01:00
parent f8e4192d34
commit ecb66fb9f3
3 changed files with 197 additions and 1 deletions

View File

@@ -0,0 +1,99 @@
This [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)-oriented API is available at the address `/rest/v0`.
### Authentication
A valid authentication token should be attached as a cookie to all HTTP
requests:
```http
GET /rest/v0 HTTP/1.1
Cookie: authenticationToken=TN2YBOMYtXB_hHtf4wTzm9p5tTuqq2i15yeuhcz2xXM
```
The server will respond to an invalid token with a `401 Unauthorized` status.
The server can request the client to update its token with a `Set-Cookie` header:
```http
HTTP/1.1 200 OK
Set-Cookie: authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs
```
Usage with cURL:
```
curl -b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs https://xo.company.lan/api/proxy/v0
```
You can use `xo-cli` to create an authentication token:
```
> xo-cli --createToken xoa.company.lan admin@admin.net
Password: ********
Successfully logged with admin@admin.net
Authentication token created
DiYBFavJwf9GODZqQJs23eAx9eh3KlsRhBi8RcoX0KM
```
### Collections
Collections of objects are available at `/<name>` (e.g. `/vms`)
Following query parameters are supported:
- `limit`: max number of objects returned
- `fields`: if specified, instead of plain URLs, the results will be objects containing the requested fields
- `filter`: a string that will be used to select only matching objects, see [the syntax documentation](https://xen-orchestra.com/docs/manage_infrastructure.html#live-filter-search)
- `ndjson`: if specified, the result will be in [NDJSON format](http://ndjson.org/)
Simple request:
```
GET /rest/v0/vms HTTP/1.1
Cookie: authenticationToken=TN2YBOMYtXB_hHtf4wTzm9p5tTuqq2i15yeuhcz2xXM
HTTP/1.1 200 OK
Content-Type: application/json
[
"/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac",
"/rest/v0/vms/5019156b-f40d-bc57-835b-4a259b177be1"
]
```
With custom fields:
```
GET /rest/v0/vms?fields=name_label,power_state HTTP/1.1
Cookie: authenticationToken=TN2YBOMYtXB_hHtf4wTzm9p5tTuqq2i15yeuhcz2xXM
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"name_label": "Debian 10 Cloudinit",
"power_state": "Running",
"url": "/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac"
},
{
"name_label": "Debian 10 Cloudinit self-service",
"power_state": "Halted",
"url": "/rest/v0/vms/5019156b-f40d-bc57-835b-4a259b177be1"
}
]
```
As NDJSON:
```
GET /rest/v0/vms?fields=name_label,power_state&ndjson HTTP/1.1
Cookie: authenticationToken=TN2YBOMYtXB_hHtf4wTzm9p5tTuqq2i15yeuhcz2xXM
HTTP/1.1 200 OK
Content-Type: application/x-ndjson
{"name_label":"Debian 10 Cloudinit","power_state":"Running","url":"/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac"}
{"name_label":"Debian 10 Cloudinit self-service","power_state":"Halted","url":"/rest/v0/vms/5019156b-f40d-bc57-835b-4a259b177be1"}
```

View File

@@ -115,6 +115,11 @@ async function updateLocalConfig(diff) {
async function createExpressApp(config) {
const app = createExpress()
// For a nicer API
//
// https://expressjs.com/en/api.html#app.set
app.set('json spaces', 2)
app.use(helmet(config.http.helmet))
app.use(compression())
@@ -237,7 +242,7 @@ async function setUpPassport(express, xo, { authentication: authCfg, http: { coo
}
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
const UNCHECKED_URL_RE = /favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/
const UNCHECKED_URL_RE = /(?:^\/rest\/)|favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/
express.use(async (req, res, next) => {
const { url } = req
@@ -771,6 +776,7 @@ export default async function main(args) {
appName: APP_NAME,
appVersion: APP_VERSION,
config,
express,
safeMode,
})

View File

@@ -0,0 +1,91 @@
import { ifDef } from '@xen-orchestra/defined'
import { invalidCredentials, noSuchObject } from 'xo-common/api-errors.js'
import { pipeline } from 'stream'
import { Router } from 'express'
import createNdJsonStream from '../_createNdJsonStream.mjs'
import pick from 'lodash/pick.js'
import map from 'lodash/map.js'
import * as CM from 'complex-matcher'
const subRouter = (app, path) => {
const router = Router({ strict: true })
app.use(path, router)
return router
}
export default class RestApi {
constructor(app, { express }) {
const api = subRouter(express, '/rest/v0')
api.use(({ cookies }, res, next) => {
app.authenticateUser({ token: cookies.authenticationToken ?? cookies.token }).then(
() => {
next()
},
error => {
if (invalidCredentials.is(error)) {
res.sendStatus(401)
} else {
next(error)
}
}
)
})
const vms = subRouter(api, '/vms')
vms.get('/', async (req, res) => {
const { query } = req
const basePath = req.baseUrl + req.path
const makeUrl = vm => basePath + vm.id
let filter
let userFilter = req.query.filter
if (userFilter) {
userFilter = CM.parse(userFilter).createPredicate()
filter = obj => obj.type === 'VM' && userFilter(obj)
} else {
filter = obj => obj.type === 'VM'
}
const vms = await app.getObjects({ filter, limit: ifDef(query.limit, Number) })
let { fields } = query
let results
if (fields !== undefined) {
fields = fields.split(',')
results = map(vms, vm => {
const url = makeUrl(vm)
vm = pick(vm, fields)
vm.url = url
return vm
})
} else {
results = map(vms, makeUrl)
}
if (query.ndjson !== undefined) {
res.set('Content-Type', 'application/x-ndjson')
pipeline(createNdJsonStream(results), res, error => {
if (error !== undefined) {
console.warn('pipeline error', error)
}
})
} else {
res.json(results)
}
})
vms.get('/:id', async (req, res, next) => {
try {
res.json(await app.getObject(req.params.id, 'VM'))
} catch (error) {
if (noSuchObject.is(error)) {
next()
} else {
next(error)
}
}
})
}
}