Tankman is a service which offers a REST API to do the following:
- Manage organizations, roles, users, resources and permissions
- Assign roles to users
- Assign user permissions and role permissions to various resources
- Attach custom properties to Orgs, Roles, Users and query them
It's important to note that Tankman does not function as an Identity and Authentication server. However, it can work well with popular open-source identity solutions such as Ory Kratos, various OAuth-based providers, or even your own custom authentication mechanism.
Tankman is available on GitHub under the MIT license. You can download it as a binary for your platform; and it utilizes PostgreSQL as its underlying database.
📢 📢 📢 Better formatted documentation can be found on the Tankman Website - tankman.dev.
Download binaries for your platform
- For most Linux distros Linux x86-64
- For Linux distros using musl Linux x86-64
- For ARM 64 bit Linux distros Linux ARM64
- macOS M1
- macOS x64
- Windows x86-64 (untested)
Step 1: Create a new database for tankman on your PostgreSQL server. Call it whatever you want, but most people call it tankmandb.
Step 2: Initialize the database. You can do that with the following command:
./tankman --initdb --dbhost YOUR_PG_HOST --dbport YOUR_PG_PORT --dbuser YOUR_PG_USER --dbpass YOUR_PG_PASSWORD
That's it. You're ready to roll.
The following command will start tankman on localhost:1989
./tankman --dbhost YOUR_PG_HOST --dbport YOUR_PG_PORT --dbuser YOUR_PG_USER --dbpass YOUR_PG_PASSWORD
You can change the hostname and port with the --host
and --port
CLI options.
./tankman --host 127.0.0.1 --port 1990 --dbhost YOUR_PG_HOST --dbport YOUR_PG_PORT --dbuser YOUR_PG_USER --dbpass YOUR_PG_PASSWORD
Instead of using CLI options as given above, you may use $TANKMAN_HOST (instead of --host
), $TANKMAN_PORT (instead of --port
) and $TANKMAN_CONN_STR (instead of --dbhost
, --dbport
, --dbuser
, --dbpass
) environment variables.
Like this:
TANKMAN_HOST=localhost
TANKMAN_PORT=1990
TANKMAN_CONN_STR=Server=localhost;Port=5432;Database=tankmandb;User Id=postgres;Password=postgres
# run tankman!
./tankman
Tankman is an internal service which should be accessible only from your backend apps. Make sure that you don't expose Tankman ports publicly.
Organizations are at the root of the entity hierarchy, and should be the first thing you should create. You may create as many orgs are you need.
HTTP POST /orgs
Payload:
{
"id": "example.com",
"data": "data for example.com"
}
Response:
{
"data": {
"id": "example.com",
"createdAt": "2023-07-02T01:32:18.4557911Z",
"data": "data for example.com",
"properties": {}
}
}
HTTP GET /orgs
Response:
{
"data": [
{
"id": "example.com",
"createdAt": "2023-07-02T01:38:18.764642Z",
"data": "data for example.com",
"properties": {}
},
{
"id": "agilehead.com",
"createdAt": "2023-07-01T01:38:18.764642Z",
"data": "some other data",
"properties": {}
}
]
}
Pagination is done with the from
and limit
query parameters.
HTTP GET /orgs?from=10&limit=100
To get a single organization (as a list with one item), specify the Org's id.
HTTP GET /orgs/{orgId}
# For example
HTTP GET /orgs/example.com
You can specify multiple Org ids.
HTTP GET /orgs/{orgId1,orgId2}
# For example
HTTP GET /orgs/example.com,northwind
HTTP PUT /orgs/{orgId}
# For example:
HTTP PUT /orgs/example.com
Payload:
{
"data": "new data for example.com"
}
Response:
{
"data": {
"id": "example.com",
"createdAt": "2023-07-02T01:38:18.764642Z",
"data": "new data for example.com",
"properties": {}
}
}
Deleting an organization will delete everything associated with it - resources, roles, users, permissions etc. This is indeed a very destructive operation.
HTTP DELETE /orgs/{orgId}
# For example:
HTTP DELETE /orgs/example.com
Since it can cause a lot of damage, there's an CLI option to require a safetyKey for deleting organizations.
You need to start tankman like this:
./tankman --safety-key $SAFETY_KEY
# For example:
./tankman --safety-key NOFOOTGUN
With the --safety-key
option, HTTP DELETE should specify the safetyKey parameter as a query string.
HTTP DELETE /orgs/{orgId}?safetyKey=$SAFETY_KEY
# For example
HTTP DELETE /orgs/example.com?safetyKey=NOFOOTGUN
You can add custom string properties to an Organization entity.
HTTP PUT /orgs/{orgId}/properties/{propertyName}
For example:
HTTP PUT /orgs/example.com/properties/country
Payload:
{
"value": "India"
}
Response:
{
"data": {
"name": "country",
"value": "India",
"hidden": false,
"createdAt": "2023-07-02T02:19:15.499711Z"
}
}
When properties are added to an Org, they are returned when the Org is fetched.
For example, GET /orgs/example.com
will retrieve the following response. Note the added properties object.
{
"data": [
{
"id": "example.com",
"createdAt": "2023-07-02T01:38:18.764642Z",
"data": "data for example.com",
"properties": {
"country": "India"
}
}
]
}
Properties are usually read as a part of the fetching entity; Org in this case. But if you need to get a list of properties without fetching the entity you can use the following API.
HTTP GET /orgs/{orgId}/properties/{propertyName}
# For example:
HTTP GET /orgs/example.com/properties/country
Response:
{
"data": [
{
"name": "country",
"value": "India",
"hidden": false,
"createdAt": "2023-07-02T02:19:15.499711Z"
}
]
}
HTTP DELETE /orgs/{orgId}/properties/{propertyName}
# For example:
HTTP DELETE /orgs/example.com/properties/country
Creating Hidden Properties
When a property is hidden, it will not be included when the organization is fetched. To create a hidden property, add the hidden flag when creating the property.
To create a Hidden Property:
HTTP PUT /orgs/{orgId}/properties/{propertyName}
# For example:
HTTP PUT /orgs/example.com/properties/revenue
Payload should include the hidden
attribute:
{
"value": "2340000",
"hidden": true
}
To include a hidden property when querying Orgs, it should be explicitly mentioned. Note that properties which aren't hidden are always included.
HTTP GET /orgs?properties={propertyName}
For example:
HTTP GET /orgs?properties={revenue}
Response:
{
"data": [
{
"id": "example.com",
"createdAt": "2023-07-02T01:38:18.764642Z",
"data": "new data for example.com",
"properties": {
"country": "India",
"revenue": "2340000"
}
}
]
}
Organizations may be filtered by the custom property.
HTTP GET /orgs?properties.{propertyName}={propertyValue}
# For example, fetch orgs with country = India
HTTP GET /orgs?properties.country=India
Roles belong to an Organization. Users can be assigned Roles. Roles can have Permissions to various Resources.
HTTP POST /orgs/{orgId}/roles
# For example:
HTTP POST /orgs/example.com/roles
Payload:
{
"id": "admins",
"data": "data for admins"
}
Response:
{
"data": {
"id": "admins",
"data": "data for admins",
"createdAt": "2023-07-02T08:31:59.7931788Z",
"orgId": "example.com",
"properties": {}
}
}
HTTP GET /orgs/{orgId}/roles/
For example:
HTTP GET /orgs/example.com/roles/
Response:
{
"data": [
{
"id": "admins",
"data": "data for admins",
"createdAt": "2023-06-30T17:01:13.525215Z",
"orgId": "agilehead.com",
"properties": {
"prop1": "value1"
}
},
{
"id": "devs",
"data": "data for devs",
"createdAt": "2023-07-01T11:56:04.039094Z",
"orgId": "agilehead.com",
"properties": {}
}
]
}
To get a single role (as a list with one item), specify the Role's id.
HTTP GET /orgs/{orgId}/roles/{roleId}
# For example
HTTP GET /orgs/example.com/roles/admins
You can specify multiple Role ids.
HTTP GET /orgs/{orgId}/roles/{roleId}
# For example
HTTP GET /orgs/example.com/roles/admins,devops
HTTP PUT /orgs/{orgId}/roles/{roleId}
# For example:
HTTP PUT /orgs/example.com/roles/admins
Payload:
{
"data": "new data for admins"
}
Response:
{
"data": {
"id": "admins",
"data": "new data for admins",
"createdAt": "2023-07-02T08:31:59.793178Z",
"orgId": "example.com",
"properties": {}
}
}
Deleting a role will also delete associated permissions.
HTTP DELETE /orgs/{orgId}/roles/{roleId}
# For example:
HTTP DELETE /orgs/example.com/roles/admins
You can add custom string properties to an Role entity.
HTTP PUT: /orgs/{orgId}/roles/{roleId}/properties/{propertyName}
For example:
HTTP PUT: /orgs/example.com/roles/admins/properties/privileged
Payload:
{
"value": "yes"
}
Response:
{
"data": {
"name": "country",
"value": "India",
"hidden": false,
"createdAt": "2023-07-02T02:19:15.499711Z"
}
}
When properties are added to an Role, they are returned when the Role is fetched.
For example, GET /orgs/example.com/roles/admins
will retrieve the following response. Note the added properties object.
{
"data": [
{
"id": "example.com",
"createdAt": "2023-07-02T01:38:18.764642Z",
"data": "data for admins",
"properties": {
"privileged": "yes"
}
}
]
}
Properties are usually read as a part of the fetching entity; Role in this case. But if you need to get a list of properties without fetching the entity you can use the following API.
HTTP GET /orgs/{orgId}/roles/{roleId}/properties/{propertyName}
# For example:
HTTP GET /orgs/example.com/properties/roles/roles/admins/privileged
Response:
{
"data": [
{
"roleId": "admins",
"orgId": "example.com",
"name": "privileged",
"value": "yes",
"hidden": false,
"createdAt": "2023-07-02T12:31:01.978925Z"
}
]
}
HTTP DELETE: /orgs/{orgId}/roles/{roleId}/properties/{propertyName}
# For example:
HTTP DELETE: /orgs/example.com/roles/admins/properties/privileged
Creating Hidden Properties
When a property is hidden, it will not be included when the Role is fetched. To create a hidden property, add the hidden flag when creating the property.
To create a Hidden Property:
HTTP PUT: /orgs/{orgId}/roles/{roleId}/properties/{propertyName}
# For example:
HTTP PUT: /orgs/example.com/roles/admins/properties/active
Payload should include the hidden
attribute:
{
"active": "yes",
"hidden": true
}
To include a hidden property when querying Roles, it should be explicitly mentioned. Note that properties which aren't hidden are always included.
HTTP GET /orgs/{orgId}/roles?properties={propertyName}
For example:
HTTP GET /orgs/example.com/roles?properties=active
Response:
{
"data": [
{
"id": "admins",
"data": "data for admins",
"createdAt": "2023-07-02T12:30:55.981226Z",
"orgId": "example.com",
"properties": {
"active": "yes",
"privileged": "yes"
}
}
]
}
Roles may be filtered by the custom property.
HTTP GET /orgs/{orgId}/roles?properties.{propertyName}={propertyValue}
# For example, fetch roles with active = yes
HTTP GET /orgs/example.com/roles?properties.active=yes
See a list of users who have a specific Role.
HTTP GET /orgs/{orgId}/roles/{roleId}/users
# For example, fetch roles with active = yes
HTTP GET /orgs/example.com/roles/admins/users
Users belong to an organization. Users may belong to Roles, and can have Permissions to various Resources.
HTTP POST /orgs/{orgId}/users
# For example:
HTTP POST /orgs/example.com/users
Payload:
{
"id": "user3",
"identityProviderUserId": "jeswinpk@agilehead.com",
"identityProvider": "Google",
"data": "data for aser3"
}
Response:
{
"data": {
"roleIds": [],
"properties": {},
"id": "user3",
"data": "data for aser3",
"identityProvider": "Google",
"identityProviderUserId": "jeswinpk@agilehead.com",
"createdAt": "2023-07-06T13:01:26.6594794Z",
"orgId": "example.com"
}
}
HTTP GET /orgs/{orgId}/users/
For example:
HTTP GET /orgs/example.com/users/
Response:
{
"data": [
{
"roleIds": [],
"properties": {},
"id": "user3",
"data": "data for aser3",
"identityProvider": "Google",
"identityProviderUserId": "jeswinpk@agilehead.com",
"createdAt": "2023-07-06T13:01:26.659479Z",
"orgId": "example.com"
}
]
}
To get a single user (as a list with one item), specify the user's id.
HTTP GET /orgs/{orgId}/users/{userId}
# For example
HTTP GET /orgs/example.com/users/user3
You can specify multiple user ids.
HTTP GET /orgs/{orgId}/users/{userId}
# For example
HTTP GET /orgs/example.com/users/user3,user5
HTTP PUT /orgs/{orgId}/users/{userId}
# For example:
HTTP PUT /orgs/example.com/users/user3
Payload:
{
"identityProviderUserId": "jeswinpk@agilehead.com",
"identityProvider": "Google",
"data": "new data for user3"
}
Response:
{
"data": {
"roleIds": [],
"properties": {},
"id": "user3",
"data": "new data for user3",
"identityProvider": "Google",
"identityProviderUserId": "jeswinpk@agilehead.com",
"createdAt": "2023-07-06T13:01:26.659479Z",
"orgId": "example.com"
}
}
Deleting a user will also delete associated user permissions.
HTTP DELETE /orgs/{orgId}/users/{userId}
# For example:
HTTP DELETE /orgs/example.com/users/user3
HTTP POST /orgs/{orgId}/users/{userId}/roles
# For example:
HTTP DELETE /orgs/example.com/users/user3/roles
Payload:
{
"roleId": "admins"
}
Response:
{
"data": {
"userId": "user3",
"roleId": "admins",
"createdAt": "2023-07-01T11:57:54.0264874Z",
"orgId": "agilehead.com"
}
}
HTTP DELETE /orgs/{orgId}/users/{userId}/roles/{roleId}
# For example:
HTTP DELETE /orgs/example.com/users/user3/roles/admins
See a list of users who have a specific Role.
HTTP GET /orgs/{orgId}/roles/{roleId}/users
# For example, fetch roles with active = yes
HTTP GET /orgs/example.com/roles/admins/users
You can add custom string properties to an user entity.
HTTP PUT: /orgs/{orgId}/users/{userId}/properties/{propertyName}
For example:
HTTP PUT: /orgs/example.com/users/user3/properties/firstName
Payload:
{
"value": "Jeswin"
}
Response:
{
"data": {
"name": "firstName",
"value": "Jeswin",
"hidden": false,
"createdAt": "2023-07-06T13:07:41.8782012Z"
}
}
When properties are added to an user, they are returned when the user is fetched.
For example, GET /orgs/example.com/users/user3
will retrieve the following response. Note the added properties object.
{
"data": [
{
"roleIds": [],
"properties": {
"firstName": "Jeswin"
},
"id": "user3",
"data": "new data for user3",
"identityProvider": "Google",
"identityProviderUserId": "jeswinpk@agilehead.com",
"createdAt": "2023-07-06T13:01:26.659479Z",
"orgId": "example.com"
}
]
}
Properties are usually read as a part of the fetching entity; user in this case. But if you need to get a list of properties without fetching the entity you can use the following API.
HTTP GET /orgs/{orgId}/users/{userId}/properties/{propertyName}
# For example:
HTTP GET /orgs/example.com/properties/users/users/user3/firstName
Response:
{
"data": [
{
"userId": "user3",
"orgId": "example.com",
"name": "firstName",
"value": "Jeswin",
"hidden": false,
"createdAt": "2023-07-06T13:07:41.878201Z"
}
]
}
HTTP DELETE: /orgs/{orgId}/users/{userId}/properties/{propertyName}
# For example:
HTTP DELETE: /orgs/example.com/users/user3/properties/firstName
Creating Hidden Properties
When a property is hidden, it will not be included when the user is fetched. To create a hidden property, add the hidden flag when creating the property.
To create a Hidden Property:
HTTP PUT: /orgs/{orgId}/users/{userId}/properties/{propertyName}
# For example:
HTTP PUT: /orgs/example.com/users/user3/properties/active
Payload should include the hidden
attribute:
{
"active": "yes",
"hidden": true
}
To include a hidden property when querying users, it should be explicitly mentioned. Note that properties which aren't hidden are always included.
HTTP GET /orgs/{orgId}/users?properties={propertyName}
For example:
HTTP GET /orgs/example.com/users?properties=active
Response:
{
"data": [
{
"roleIds": [],
"properties": {
"active": "yes",
"firstName": "Jeswin"
},
"id": "user3",
"data": "new data for user3",
"identityProvider": "Google",
"identityProviderUserId": "jeswinpk@agilehead.com",
"createdAt": "2023-07-06T13:01:26.659479Z",
"orgId": "example.com"
}
]
}
users may be filtered by the custom property.
HTTP GET /orgs/{orgId}/users?properties.{propertyName}={propertyValue}
# For example, fetch users with active = yes
HTTP GET /orgs/example.com/users?properties.active=yes
Response:
{
"data": [
{
"roleIds": [],
"properties": {
"firstName": "Jeswin"
},
"id": "user3",
"data": "new data for user3",
"identityProvider": "Google",
"identityProviderUserId": "jeswinpk@agilehead.com",
"createdAt": "2023-07-06T13:01:26.659479Z",
"orgId": "example.com"
}
]
}
Resources belong to Organizations - and they're names of various entities in the Organization. We use Unix-like paths to represent entities in an organization.
Let's see some examples.
Here's how you'd store some file paths.
- /files/legal/abc.doc
- /files/marketing/another.html
But anything can be a path. For example, if you wanted to control access to a feature, you could define a path as follows and check a user's permissions to it.
- /features/sellbitcoin
- /features/bulkbuy
HTTP POST /orgs/{orgId}/resources
For example:
HTTP POST /orgs/example.com/resources
Payload:
{
"id": "/root/drives/c/home",
"data": "data for /root/drives/c/home"
}
Response:
{
"data": {
"id": "/root/drives/c/home",
"data": "data for /root/drives/c/home",
"createdAt": "2023-07-06T13:40:03.4217161Z",
"orgId": "example.com"
}
}
HTTP GET /orgs/{orgId}/resources
For example:
HTTP GET /orgs/example.com/resources
Response:
{
"data": [
{
"id": "/root/drives/c/home",
"data": "data for /root/drives/c/home",
"createdAt": "2023-07-06T13:40:03.421716Z",
"orgId": "example.com"
}
]
}
Pagination is done with the from
and limit
query parameters.
HTTP GET /orgs/example.com/resources?from=10&limit=100
To get a single resource (as a list with one item), specific the Resource's path.
HTTP GET /orgs/{orgId}/resources/{resourcePath}
# For example
HTTP GET /orgs/example.com/resources/root/drives/c/home
A wildcard search allows you to search for all resources starting with a certain path. The wildcard character to use is a tilde(~).
HTTP GET /orgs/{orgId}/resources/{basePath}/~
# For example
HTTP GET /orgs/example.com/resources/root/drives/~
Assume you have the following resources:
- /orgs/example.com/resources/root/drives/c/home
- /orgs/example.com/resources/root/drives/d/home
The request HTTP GET /orgs/example.com/resources/root/drives/~
will fetch both since they start with /root/drives/
.
HTTP PUT /orgs/{orgId}/resources/{resourcePath}
For example:
HTTP PUT /orgs/example.com/resources/root/drives/c/home
Payload:
{
"data": "new data for /root/drives/c/home"
}
Response:
{
"id": "/root/drives/c/home",
"data": "new data for /root/drives/c/home",
"createdAt": "2023-06-30T13:39:29.182724Z",
"orgId": "agilehead.com"
}
HTTP DELETE /orgs/{orgId}/resources/{resourcePath}
For example:
HTTP DELETE /orgs/example.com/resources/root/drives/c/home
Permissions are entities which determine whether a Role or a User has access to a Resource. Permissions also have an "action" property which specifies what the Role or User is allowed to do with the Resource.
HTTP POST /orgs/{orgId}/roles/{roleId}/permissions
For example:
HTTP POST /orgs/example.com/roles/admins/permissions
Payload:
{
"resourceId": "/root/drives/c/home",
"roleId": "admins",
"action": "write"
}
Response:
{
"roleId": "admins",
"resourceId": "/root/drives/c/home",
"action": "write",
"createdAt": "2023-06-30T14:04:17.919562Z",
"orgId": "agilehead.com"
}
The example above specifies that the role "admins" can "write" to the resource "/root/drives/c/home". Note that the resource should already exist.
HTTP POST /orgs/{orgId}/users/{userId}/permissions
For example:
HTTP POST /orgs/example.com/users/user3/permissions
Payload:
{
"resourceId": "/root/drives/c/home",
"userId": "user3",
"action": "read"
}
Response:
{
"userId": "admins",
"resourceId": "/root/drives/c/home",
"action": "write",
"createdAt": "2023-06-30T14:04:17.919562Z",
"orgId": "agilehead.com"
}
The example above specifies that the user "user3" can "read" the resource "/root/drives/c/home".
Effective Permissions for a user is the combined list of all user permissions defined for the specifc user, and role permissions for the roles in which the user is a member.
For example, if the user "john" is a member of roles "admins" and "devops", effective permissions to a resource is the combined list of john's permissions to the specific resource, the role "admins" permissions to the resource, and the role "devops" permissions to the resource.
HTTP GET /orgs/{orgId}/users/{userId}/effective-permissions/{action}/{resourceId}
For example:
HTTP GET /orgs/example.com/users/user3/effective-permissions/write/root/drives/c/home
Response:
[
{
"roleId": "admins",
"resourceId": "/root/drives/c/home",
"action": "write",
"createdAt": "2023-06-30T14:04:17.919562Z",
"orgId": "agilehead.com"
}
]
You can specify a wildcard (tilde "~", by default) to get all permissions irrespective of action.
For example, the following will fetch read and write actions:
HTTP GET /orgs/example.com/users/user3/effective-permissions/~/root/drives/c/home
Response:
[
{
"userId": "user3",
"resourceId": "/root/drives/c/home",
"action": "read",
"createdAt": "2023-06-30T14:04:01.837162Z",
"orgId": "agilehead.com"
},
{
"roleId": "admins",
"resourceId": "/root/drives/c/home",
"action": "write",
"createdAt": "2023-06-30T14:04:17.919562Z",
"orgId": "agilehead.com"
}
]
You can specify a wildcard in the resource path as well.
For example, the following will return all resources starting with "/root/drives". Note that we've used a wild card action as well.
HTTP GET /orgs/example.com/users/user3/effective-permissions/~/root/drives/~
You can get permissions for a role to a resource thus.
HTTP GET /orgs/{orgId}/roles/{roleId}/permissions
For example:
HTTP GET /orgs/example.com/roles/admins/permissions
Response:
{
"data": {
"roleId": "admins",
"resourceId": "/root/drives/c/home",
"action": "write",
"createdAt": "2023-07-06T14:35:31.3918132Z",
"orgId": "example.com"
}
}
You can get permissions for a user to a resource thus.
HTTP GET /orgs/{orgId}/users/{userId}/permissions
For example:
HTTP GET /orgs/example.com/users/user3/permissions
Response:
{
"data": {
"userId": "user3",
"resourceId": "/root/drives/c/home",
"action": "read",
"createdAt": "2023-07-06T14:35:31.3918132Z",
"orgId": "example.com"
}
}
You can delete permissions for a role to a resource thus.
HTTP DELETE /orgs/{orgId}/roles/{roleId}/permissions/{action}/{resourceId}
For example:
HTTP DELETE /orgs/example.com/roles/admins/permissions/read/root/drives/c/home
Response:
{
"data": {
"roleId": "admins",
"resourceId": "/root/drives/c/home",
"action": "write",
"createdAt": "2023-07-06T14:35:31.3918132Z",
"orgId": "example.com"
}
}
You can delete permissions for a user to a resource thus.
HTTP DELETE /orgs/{orgId}/users/{userId}/permissions/{action}/{resourceId}
For example:
HTTP DELETE /orgs/example.com/users/admins/permissions/read/root/drives/c/home
Response:
{
"data": {
"userId": "admins",
"resourceId": "/root/drives/c/home",
"action": "write",
"createdAt": "2023-07-06T14:35:31.3918132Z",
"orgId": "example.com"
}
}
- Test Suite. We need to build a full test suite. This is our highest priority item.
- Publish to Docker Hub
- Docker compose file with PostgreSQL
- Clean up the Documentation
- Document patterns to use for Authorization.