- Description
- Requirements
- Building and Testing
- Configuration
- Running in Containers
- Tools
- LICENSE
- REST API Documentation
The Service Information System (SIS) is a CMDB alternative designed with collaboration and customizability in mind. Access to all data is strictly via the familiar principles of REST over HTTP.
The SIS API allows you to interact with SIS data using anything that can issue an HTTP request and parse JSON. The API allows clients to do a variety of operations including:
- CRUD object definitions. SIS refers to these as
schemasand map to concepts like DB Tables or object classes. - CRUD instances of objects of a
schema. These are referred to asentitiesin SIS. - Register for notifications when any SIS object is created, modified, or deleted.
In SIS terms, these are called
hooks. - Retrieve the state of a SIS object at a particular moment in time.
- Control access to objects and ensure the right users can manage them.
- Access the API from any website using a browser that supports CORS.
More details can be found in the API Documentation
The following are required to build and run SIS:
- Node.js 20 or later (see the
enginesfield inpackage.json). - MongoDB 4.4+ (tested against MongoDB 7).
If you would rather not install these locally, see
Running in Containers to run the API and MongoDB with
podman / docker.
SIS uses npm scripts for its tooling: Mocha for tests, ESLint for linting, and c8 for coverage. (The former grunt + jshint + blanket stack has been removed.)
- Ensure a local MongoDB instance is running on port 27017.
mongod --dbpath=/data --port 27017will launch a mongo server listening on 27017 and writing data to/data.- Alternatively, start one in a container (see Running in Containers).
- From the root directory, run
npm install. - Run
npm run gen-jwt-keyonce to create the key pair used to sign session tokens (see Session Tokens). - Run
npm start(ornode server.js) to start the server. The main file isserver.js.
The available npm scripts are:
npm start- run the server (node server.js).npm test- run the Mocha suite (config in.mocharc.yml).npm run lint- run ESLint (flat config ineslint.config.js).npm run coverage- run the test suite underc8and emit an HTML + text report.npm run build- generatebuild.json(consumed by the/infoendpoint).npm run gen-jwt-key [kid]- generate an Ed25519 key pair inconf/keys/for signing session tokens.
The test suite connects to mongodb://localhost/sis-test by default. Override
the connection string with the db__url environment variable (nconf reads
__ as the nested-key separator):
db__url="mongodb://localhost:27018/sis-test" npm testTo run a single spec, list test/init-tests.js first (it starts the shared
server and seeds a superuser) followed by the target spec:
db__url="mongodb://localhost/sis-test" ./node_modules/.bin/mocha --timeout 60000 \
test/init-tests.js test/v1.1-api/test-api-entities.jsRelevant environment variables:
SIS_RUN_LONG_TESTS=true- enable the longer/replication suites.SIS_REMOTE_URL- run the suite against a remote SIS instead of spawning a local server.db__url- override the MongoDB connection string for a run.
The default configuration lives in conf/config.json. Settings are resolved
with the following precedence (highest first):
- Environment variables (
nconfreads__as the nested-key separator, sodb.urlis set viadb__url; values are parsed as JSON). - Command-line arguments.
conf/config.json.local- an optional, git-ignored file for local overrides.conf/config.json- the committed defaults.
An annotated config.json looks like:
{
// database specific configuration
"db": {
// a mongo db connection string
"url" : "mongodb://localhost/sis"
},
// server specific config
"server" : {
// the tcp port to listen on
"port" : 3000
},
// application specific settings
"app" : {
// whether Role Based Access Control is enabled. Defaults to true
"auth" : true,
// whether read (GET) requests also require a valid API token.
// Requires "auth" to be true. Defaults to false (reads are public)
"auth_reads" : false,
// authentication backend config. See below
// defaults to a sis backend.
"auth_config" : {
// specific info per backend
},
// session token (JWT) settings. See "Session Tokens" below
"jwt" : {
// key id used to sign new session tokens. Defaults to "default"
"active_kid" : "default",
// directory holding jwt-<kid>.key / jwt-<kid>.pub, relative to
// conf/ unless absolute. Defaults to "keys" (conf/keys)
"keys_dir" : "keys",
// session length in ms. Defaults to 8 hours
"ttl_ms" : 28800000,
// the iss claim. Defaults to "sis"
"issuer" : "sis"
},
// whether the app only serves read requests (GET). Defaults to false
"readonly" : false,
// whether to enable custom scripts support
"scripts_enabled" : false,
// minimum workers to run scripts in pool
"scripts_min" : 1,
// maximum workers to run scripts in pool
"scripts_max" : 2,
// time that a worker can remain idle before being collected
"script_worker_idle_time_ms" : 300000,
// use the cluster module
"use_cluster" : true,
// optional read preference to use on GET requests
"get_read_pref" : "nearest"
},
// default logger options - see bunyan configuration
"logger" : {
"options" : {
"name" : "SIS",
"level" : "debug",
"streams" : [{
"path" : "./sis.log"
}]
}
},
}The default backend authenticates a user against a password stored with the user object. Password hashes are stored in SIS if this backend is used. Configure using:
"auth_config" : {
"type" : "sis"
}The LDAP authentication backend authenticates a user by attempting an LDAP bind with their credentials. When a user successfully authenticates for the first time, the backend creates an empty User object with no privileges.
Configure the backend using:
"auth_config" : {
"type" : "ldap",
"url" : "<url of the LDAP endpoint>",
"user_domain" : "<the user domain to authenticate against>",
"email_domain" : "<the domain to append as the email address for the user>",
"client_opts" : {
"option_1" : "option_1_value"
// etc. These are options passed to the ldapts Client constructor:
// https://github.com/ldapts/ldapts
}
}By default SIS binds using an Active Directory userPrincipalName, i.e.
<user>@<user_domain>. For example, with:
"auth_config" : {
"type" : "ldap",
"url" : "ldap://10.1.1.1",
"user_domain" : "ad.corp.com",
"email_domain" : "company.com"
}An authentication request for user1 will attempt to bind as user1@ad.corp.com
and create the user user1 with email user1@company.com if successful and does
not already exist.
Directories such as OpenLDAP bind by a full distinguished name rather than a
userPrincipalName. For those, set bind_dn_template instead of (or in addition
to) user_domain. The %u placeholder is replaced with the (DN-escaped) login
name:
"auth_config" : {
"type" : "ldap",
"url" : "ldap://openldap:1389",
"email_domain" : "example.org",
"bind_dn_template" : "cn=%u,ou=users,dc=example,dc=org"
}An authentication request for user1 then binds as
cn=user1,ou=users,dc=example,dc=org. When bind_dn_template is set,
user_domain is optional. See
Trying the LDAP backend for a ready-to-run example.
POST /api/v1.1/users/auth_token returns a session token: a JWT signed with an
Ed25519 key, valid for 8 hours by default. Session tokens are not stored in the
database; persistent API tokens (/users/:name/tokens) are unchanged. Either
kind can be sent in the x-auth-token header or as Authorization: Bearer <token>.
See Tokens for the API.
The signing keys are stored alongside the configuration:
conf/
config.json
keys/
jwt-default.key (private key, mode 0600, git-ignored)
jwt-default.pub (public key)
Create them with:
npm run gen-jwt-key # writes conf/keys/jwt-default.key and .pub(With Compose the keys are created for you; see Running in Containers.)
The server refuses to start when authentication is enabled and the private key
for app.jwt.active_kid is missing, and logs a warning if that file is readable
by group or others. Every jwt-*.pub file in keys_dir is trusted for
verification. In clustered or replicated deployments, every server must have
the same key files. Servers running with "readonly" : true need only the
.pub files; without a private key they cannot hand out sessions.
- Generate a new pair with a new key id:
npm run gen-jwt-key 2027-01. - Copy
jwt-2027-01.pubto every server, thenjwt-2027-01.keyto the servers that issue sessions. - Set
app.jwt.active_kidto2027-01(orapp__jwt__active_kid) and restart. Existing sessions keep working because the old.pubis still present. - After the session length (8 hours) has passed, delete the old
.keyand.pub.
DELETE /api/v1.1/users/auth_token ends every session of the calling user.
Changing a user's password, or deleting the user, also ends their sessions.
Role changes take effect on the next request.
SIS ships with a Dockerfile and a compose.yaml so the API and its MongoDB
dependency can be run as containers. The examples below use podman /
podman compose; substitute docker / docker compose if you prefer - the
files are compatible with both.
compose.yaml runs mongo (MongoDB 7), sis-api (this app), and sis-keys, a
one-shot job that creates the session signing keys on
the first start and does nothing on later starts. The API waits for MongoDB to
report healthy and for sis-keys to finish before starting.
# Build the image and start the services in the background
podman compose up -d --build
# Follow the API logs (JSON, written to stdout)
podman compose logs -f sis-api
# Stop the stack (add -v to also delete the MongoDB data volume)
podman compose downOnce up, the API listens on http://localhost:3000:
curl -I http://localhost:3000/ # -> 200
curl http://localhost:3000/api/v1.1/info # -> build/version infoThe signing keys live next to the config at /app/conf/keys, stored in the
named sis_keys volume (mounted read-only into sis-api). conf/keys/ is
excluded from the image by .dockerignore, so private keys never end up in an
image layer. To rotate, generate a new pair into the volume and set
app__jwt__active_kid on sis-api:
podman compose run --rm sis-keys node scripts/gen-jwt-key.js 2027-01To manage the key files yourself instead, replace the volume with a bind mount
of a host directory. The files must be readable by the container's node user
(uid 1000), so chown 1000 the private key rather than loosening its mode.
MongoDB data persists in the named mongodb_data volume. By default MongoDB is
reachable only by the sis-api service over the internal Compose network and is
not published to the host; uncomment the ports block on the mongo service in
compose.yaml if you need to connect to it directly.
The image contains the default conf/config.json. Rather than editing files,
override individual settings with environment variables: nconf reads env vars
using __ as the nested-key separator, so db.url becomes db__url,
server.port becomes server__port, and so on. Values are parsed as JSON, so
"false" and "3000" become a boolean and a number respectively.
compose.yaml uses this to point the API at the mongo service and to run a
single process per container:
environment:
db__url: "mongodb://mongo:27017/sis"
server__port: "3000"
app__use_cluster: "false"app.use_cluster is disabled in containers because the cluster module forks a
worker per host CPU (ignoring container CPU limits); run one process per
container and scale with replicas instead.
podman build -t sis-api:latest .
# create the session signing keys in a volume (once)
podman run --rm -v sis_keys:/app/conf/keys sis-api:latest \
node scripts/gen-jwt-key.js --if-missing
podman run --rm -p 3000:3000 \
-e db__url="mongodb://my-mongo-host:27017/sis" \
-v sis_keys:/app/conf/keys:ro \
sis-api:latestThe build generates build.json (served by the /info endpoint). Pass
--build-arg BUILD_NUMBER=... --build-arg GIT_COMMIT_HASH=... to stamp CI build
metadata into the image.
The image runs as the non-root node user and sets STOPSIGNAL SIGINT so that
podman stop / docker stop triggers the server's graceful-shutdown handler.
Note: MongoDB runs without authentication in the default
compose.yaml, which matches the credential-lessdb.urland is fine for local development. For a deployment that requires auth, enable it on themongoservice and put the credentials indb__url(e.g.mongodb://user:pass@mongo:27017/sis?authSource=admin).
An optional overlay, compose.ldap.yaml, adds a throwaway OpenLDAP server
(seeded with a user1 / pass1 account) and switches sis-api to the
LDAP auth backend. It is kept separate so the
default podman compose up stays on the built-in sis backend.
Start the stack with both files:
podman compose -f compose.yaml -f compose.ldap.yaml up -d --buildThen mint a token as the seeded directory user to confirm LDAP auth works:
curl -u user1:pass1 -X POST http://localhost:3000/api/v1.1/users/auth_token
# -> 201 with a session token (a JWT in "name"); SIS auto-creates the user1
# account on first loginThe overlay points SIS at the LDAP server with a bind-DN template (see Binding by full DN):
app__auth_config__type: "ldap"
app__auth_config__url: "ldap://ldap:1389"
app__auth_config__email_domain: "example.org"
app__auth_config__bind_dn_template: "cn=%u,ou=users,dc=example,dc=org"This overlay is for local development only. It uses Bitnami's frozen
bitnamilegacy/openldap image and ships throwaway credentials in the compose
file; do not use it as-is in production.
This section documents some tools provided in the source.
After SIS is up and running, creating an initial super user is the next logical step. The initial user must be created via command line as there are no users who are permitted to use the REST API. To create a super user, create a json file that looks like the following:
{
"name" : "a_super_user",
"pw" : "super_user_pw",
"email" : "super@user.com",
"super_user" : true
}Then run node tools/useradmin.js update /path/to/json/file
Similar to creating a user, create a JSON file with the following contents
{
"name" : "user_to_delete"
}Then run node tools/useradmin.js delete /path/to/json/file
This software is licensed under the BSD 3-Clause license. Please refer to the LICENSE for more information.
Detailed documentation can be found in the docs folder.