Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 14 additions & 19 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,26 @@
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach to current script",
"protocol": "inspector",
"port": 4321,
"restart": true,
"cwd": "${workspaceRoot}"
},
{
"type": "node",
"name": "Debug src/app.ts",
"type": "pwa-node",
"request": "launch",
"name": "Debug npm run dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "dev"],
"runtimeExecutable": "node",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"args": ["src/app.ts"],
"cwd": "${workspaceRoot}",

"console": "integratedTerminal"
"internalConsoleOptions": "openOnSessionStart",
"skipFiles": ["<node_internals>/**", "node_modules/**"],
"env": {
"NODE_ENV": "test"
},
"resolveSourceMapLocations": ["!${workspaceFolder}/**"]
},
{
"type": "node",
"request": "launch",
"name": "Debug npm run dev:no-authz",
"name": "Debug npm run dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "dev:no-authz"],
"runtimeArgs": ["run-script", "dev"],
"cwd": "${workspaceRoot}",

"console": "integratedTerminal"
Expand All @@ -41,8 +37,7 @@
},
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/ts-mocha",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"protocol": "inspector"
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
Expand Down
76 changes: 75 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ method.
For the api client there is an `operationId` property defined. It can be used to client with expected method names (see
relevant usage in otomi-web repo)

### 1.2 Authentication
### 2.2 Authentication

The authentication ensures that a user is identified, so the request contains required headers.

Expand Down Expand Up @@ -193,6 +193,80 @@ The `TeamSelfService` schema is composed by:
- property that corresponds to a schema name from `api.yaml` file.
- `enum` property that indicates JSON paths for attributes that shall be controlled.

**Note:**

- `delete` permission cannot be set for ABAC

For example:

```
Service:
x-acl:
admin: [delete-any, read-any, create-any, update-any]
team: [delete, read, create, update]
type: object
properties:
name:
type: string
ingress:
type: object
x-acl:
admin: [read, create]
team: [read]
```

From above:

A user with admin role can:

- perform all CRUD operations regardless resource ownership (RBAC)
- all attributes can be edited except ingress that can be only set on resource creation event (ABAC)

A user with team role can:

- perform all CRUD operations only withing its own team (RBAC)
- all attributes can be edited except ingress that isn be only read (ABAC)

#### 2.3.3 Limitations

##### 2.3.3.1 OpenAPI-generator limitations

<!-- Add more issues if you spot them and know the limitations/work-arounds -->

Known issues:

- https://github.com/redkubes/otomi-api/issues/155

###### Problem

It doesn't matter if you've entered a valid OpenAPI specification, it isn't useful as long as it isn't generated as a client library.

###### Cause

There are too many variations of this problem to be listed here and still make sense, but they follow the following cycle in general:

1. `src/openapi/\*.yaml` cannot be dereferenced/bundled by parsing JSON `$refs`.
2. Dereferenced/bundled OpenAPI spec cannot be generated
3. Client libary in `vendors/client/otomi-api/axios/...` cannot be compiled with `tsc`
4. Code cannot be committed in version control (Git)
5. Consume API methods and/or models

###### Solutions

In this paragraph the causes are addressed by the corresponding number under "Cause":

1. In the `npm run ...` scripts, `vendors/openapi/otomi-api.json` may be deleted to see if the spec can be successfully dereferenced/bundled and used as input for `openapi-generator`.
2. The `openapi-generator` can throw useful/meaningful errors. But there are errors under known issues (see above) that need a work-around.
3. These errors happen the most arbitrarily. See if you can go back in your small increments in `src/openapi/...` until you can successfully build the client library again.
4. These errors are often due to our own code. E.g.: a generated model is used, and by changing the OpenAPI spec you change the schema. Models used to rely on the schema and now they are missing.
5. If you change the name of a schema, add a title, etc., the respective reference might change. Then the consumption in the API library might break.

###### Note

- Also check if you can successfully generate the client library again after committing, just as a pre-caution.
- To determine a successful generation of the client library, please check out the generated models in `vendors/client/otomi-api/axios/models` if they make sense or not.
- As general advice, make sure to increment the specification VERY slowly and always see if a spec can be generated or not.

## 2. Viewing/consuming openapi spec

In order to inspect the api file it is recommended to either:
Expand Down
20 changes: 13 additions & 7 deletions bin/generate-client.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ validate() {
fi
}

clean_up() {
if [ -f vendors/openapi/otomi-api.json ]; then
rm vendors/openapi/otomi-api.json
fi
rm -rf vendors/client/otomi-api >/dev/null
}

generate_client() {
echo "Generating client code from openapi specification $openapi_doc.."
rm -rf $target_dir >/dev/null
echo "Generating client code from openapi specification $openapi_doc..."

# npx openapi bundle --output src/openapi --ext yaml src/openapi/api.yaml
npm run build:spec

docker run --rm -v $PWD:/local -w /local -u "$(id -u $USER)" \
openapitools/openapi-generator-cli:v5.1.0 generate \
Expand All @@ -42,7 +49,7 @@ generate_client() {
}

set_package_json() {
echo "Updating $target_package_json file.."
echo "Updating $target_package_json file..."

jq \
--arg type 'git' \
Expand All @@ -68,17 +75,16 @@ set_bluebird() {
}

build_npm_package() {
echo "Building $target_npm_name npm package"
cd $target_dir
cd $target_dir && echo "Building $target_npm_name npm package..."
npm install && npm run build
cd -
}

rm -rf $target_dir >/dev/null
validate
clean_up
generate_client
set_package_json
set_bluebird
build_npm_package

echo "The client code has been generated at $target_dir/ directory"
echo "The client code has been generated at $target_dir/ directory."
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,15 @@
"build": "tsc && copyup --error src/openapi/*.yaml src/*.json dist/src",
"build:models": "npm run build:spec && openapi-typescript src/generated-schema.json -o src/generated-schema.ts",
"build:spec": "ts-node-dev src/build-spec.ts",
"build:client": "npm run build:spec && rm -rf vendors/client/otomi-api >/dev/null && bin/generate-client.sh",
"build:client": "bin/generate-client.sh",
"build:pre-commit": "npm run build:models && bin/generate-client.sh",
"cz": "git-cz",
"cz:retry": "git-cz --retry",
"dev:watch": "run-p dev watch:openapi watch:ts",
"dev": "ts-node-dev --watch 'src/openapi/*.yaml' --inspect=4321 --respawn --transpile-only src/app.ts",
"dev:docker": "npm install && npm run dev",
"husky:lint-staged": "lint-staged",
"husky:pre-commit": "run-p lint husky:lint-staged build:models",
"husky:pre-commit": "npm run build:pre-commit && npm run lint && npm run husky:lint-staged",
"lint": "run-p types lint:es",
"lint:es": "eslint --ext ts .",
"lint:fix": "eslint --ext ts --fix .",
Expand All @@ -159,4 +160,4 @@
}
},
"version": "0.4.53"
}
}
111 changes: 69 additions & 42 deletions src/api.authz.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,76 @@ describe('Admin API tests', () => {
sinon.stub(otomiStack)
app = await initApp(otomiStack)
})
it('admin can get all settings', (done) => {
request(app)
.get('/v1/settings')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200)
.end(done)

describe('Admin /settings/{setting} endpoint tests', () => {
const endpoints = ['alerts', 'azure', 'customer', 'dns', 'kms', 'home', 'oidc', 'otomi', 'smtp']
endpoints.forEach((ep) => {
it(`admin can get /settings/${ep}`, (done) => {
request(app)
.get(`/v1/settings/${ep}`)
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200)
.expect('Content-Type', /json/)
.end(done)
})
})

it('admin can put /settings/alerts with correct payload', (done) => {
request(app)
.put('/v1/settings/alerts')
.send({
alerts: {
drone: 'msteams',
groupInterval: '5m',
msteams: {
highPrio: 'bla',
lowPrio: 'bla',
},
receivers: ['slack'],
repeatInterval: '3h',
},
})
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200)
.end(done)
})
it('admin cannot put /settings/alerts with extra properties', (done) => {
request(app)
.put('/v1/settings/alerts')
.send({
alerts: {
drone: 'msteams',
groupInterval: '5m',
msteams: {
highPrio: 'bla',
lowPrio: 'bla',
},
receivers: ['slack'],
repeatInterval: '3h',
randomProp: 'randomValue',
},
})
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${adminToken}`)
.expect(400)
.end(done)
})

it('admin can put empty payload, but it wont change anything', (done) => {
request(app)
.put('/v1/settings/dns')
.send({
zones: ['someZoneOne', 'someZoneTwo'],
})
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${adminToken}`)
.expect(400)
.end(done)
})
})

it('admin can update team self-service-flags', (done) => {
request(app)
.put('/v1/teams/team1')
Expand All @@ -39,41 +101,6 @@ describe('Admin API tests', () => {
.expect(200)
.end(done)
})

it('admin can put with payload that matches the schema', (done) => {
request(app)
.put('/v1/settings')
.send({
alerts: {
drone: 'msteams',
},
})
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200)
.expect('Content-Type', /json/)
.end(done)
})
it('admin can put with empty body (empty object is valid JSON Schema 7)', (done) => {
request(app)
.put('/v1/settings')
.send({})
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200)
.expect('Content-Type', /json/)
.end(done)
})
it(`admin can't put with keys that don't match settings object`, (done) => {
request(app)
.put('/v1/settings')
.send({ foo: 'bar' })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${adminToken}`)
.expect(400)
.expect('Content-Type', /json/)
.end(done)
})
it('admin can get all teams', (done) => {
request(app)
.get('/v1/teams')
Expand Down
19 changes: 0 additions & 19 deletions src/api/settings.ts

This file was deleted.

23 changes: 23 additions & 0 deletions src/api/settings/{setting}.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Operation, OperationHandlerArray } from 'express-openapi'
import OtomiStack from '../../otomi-stack'
import { OpenApiRequest } from '../../otomi-models'

export default function (otomi: OtomiStack): OperationHandlerArray {
const GET: Operation = [
({ params: { setting } }: OpenApiRequest, res): void => {
console.debug(`Get settings: ${JSON.stringify({ setting })}`)
res.json(otomi.getSubSetting('settings', setting))
},
]
const PUT: Operation = [
({ params: { setting }, body }: OpenApiRequest, res): void => {
console.debug(`Modify settings: ${JSON.stringify({ setting })}`)
res.json(otomi.setSubSetting('settings', body, setting))
},
]
const api = {
GET,
PUT,
}
return api
}
1 change: 0 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,5 @@ otomiStack
})
.catch((e) => {
console.error(e)
server.close()
process.exit(1)
})
Loading