The Multi-Purpose Software License Manager (SLM, for short) is a web application that is meant to facilitate the management of software distribution.
- Introduction
- Getting Started
- Documentation
3.1 Directory Structure
3.2 Database
3.3 RESTful API - Helping out
SLM was initially developed as part of the Captsone Project for the Bachelors of Informatic Engineering of the University of Porto. Taking place as a piece of work for BUILT-CoLAB, the project became open-source and is open for improvements and commercial use as per the terms of the MIT License agreement.
SLM is a Web Application whose main purpose is to help license distributors to manage their existing products, customers and licenses alike. Nowadays, the extreme complexity of the market and the need to reduce the frequency of piracy requires a concise tool that is clear, objective and straightforward. This project was created as a solution to these issues by using a more user-friendly interface that allows for the management of important data features and to observe statistics that are important from a marketing point of view.
The implementation of the web application uses the Flask microframework, in Python 3.9. As a distributor you do not need to do anything other than following the first section of this introductory file. However, as a developer, you are free to check out this entire file in order to learn how to customize / improve the features that SLM offers.
The application has a rich interface and allows license distributors to add as many Products and License Keys as they wish. These same License Keys can be edited, deleted or even disabled. All these modifications are also saved in a Changelog to prevent misuse and allow managers and customer support agents to keep track of what happens to each License. For security reasons, the Changelogs are irremovable and the actions of local administrators will always be registered into the log storage. Apart from these features, administrators can also create Customers, store their contact information or even remove them from the service should they ever request that.
As it stands, the SLM aims to offer the essential features of any common License Managing software while keeping its Multi-Purpose nature and carrying a unique GUI.
In order to get everything ready you are required to have Docker installed. After doing that, and cloning the project from this repository, you should simply follow the following steps:
Step 1: Build the docker image
docker build --tag license-manager .
Step 2: Run it with the command
Linux (Bash):
docker run -v $(pwd)/src/database:/license-manager/src/database -p 8000:8000 license-manager
Windows (Powershell):
docker run -v ${PWD}/src/database:/license-manager/src/database -p 8000:8000 license-manager
Optional docker envs: --env WORKERS=2 --env THREADS=4 --env PORT=8000
After doing these steps, the project should be available at http://localhost:8000/
.
Step 3: To stop the image from running simply run
docker stop $(docker ps -q --filter ancestor=license-manager )
By default, the login data is defined as:
Username: root
Password: root
In order to change these login details, you will need to modify the .env variable, by changing the ADMINUSERNAME
and ADMINPASSWORD
fields, respectively.
This section is meant to help developers and contributors to modify, customize and augment this project in any way that is suitable for them.
As mentioned in previously, the Project uses FLASK, a micro web framework that is written in Python. All the necessary contents are stored in the src
folder.
In a more graphical way, the directory structure is represented bellow, with some comments to help you out.
.
└── src
└── handlers # The respective handler for each service route in the RESTful API (called by main.py).
├── admins.py
├── changelogs.py
├── customers.py
├── licenses.py
├── logs.py
├── products.py
├── utils.py
└── validation.py
└── static # The containing folder for images, style sheets and javascript.
├── dark.mode.handler.js
├── dashboardImage.jpg
├── default.jpg
├── flowbite.min.css
├── loginImage.svg
├── logoComplete.svg
├── style.css
├── tailwind.js
├── typography.min.css
└── wallpaperchangelog.jpg
└── templates # All pages (.html files) are placed in this folder (called by FLASK when it renders a page).
├── 404.html
├── base.html
├── changelog.html
├── cpanel.html
├── customers.html
├── index.html
├── license.html
├── product.html
├── products.html
├── tutorial.html
└── users.html
├── __init__.py # FLASK settings
├── auth.py # API Authentication Endpoints are defined here.
├── databaseAPI.py # "Proxy" for database communication.
├── keys.py # License methods are defined here.
├── main.py # General API Endpoints are defined here.
├── models.py # ORM definition of the Database (SQLAlchemy).
└── sqlite.db # The SQLite database file.
└── client
└── clientTest.py # A mock-up of a software trying to communicate with the web application.
└── docs
└── ............. # Used for pedagogical terms and evaluation
└── tests
└── api # API Tests
└── __init__.py
└── test_admin_functionality.py
└── test_customer_functionality.py
└── test_license_functionality.py
└── test_product_functionality.py
└── test_security.py
└── test_validation_functionality.py
└── unit # Unit Tests
└── test_functions.py
└── test_models.py
└── __init__.py
└── conftest.py # Pytest main configuration file
├── .env # Environment Variables used by Docker
├── Dockerfile # The Dockerfile used to set-up the Docker Image.
├── README.md # This documentation.
├── LICENSE.md # MIT License statement
├── requirements.txt # Used by the pip3 command to install all dependencies
└── server.py # Python script that runs the web application
The database used in SLM is the SQLite database. The file is present right in the first level of the src
directory. It is automatically generated by the application if the file does not yet exist. Note, however, that this only occurs once every time you run the application. Deleting the file while the web app is running will effectively render the entire application useless until you restart it.
Also, be aware that when you delete this file, ALL data will be lost. This includes (but is not limited to): User accounts, Products, Licenses, Registered Devices, Customers and even the Changelog. Doing so, will reset the entire project as if you were opening it for the first time. We suggest you to create a routine that generates a backup every now and then in order to prevent the loss of critical data.
When it comes to the documentation of the database, you can check its structure in the models.py
file. However, in order to help you get through the SQLAlchemy's syntax, we will represent the same information in the file in a tabular format:
USER Table | Type | PK | UQ | AI | ONDELETE |
---|---|---|---|---|---|
id | INT | X | X | NONE | |
TEXT | X | NONE | |||
password | TEXT | NONE | |||
name | TEXT | X | NONE | ||
owner | BOOL | NONE | |||
disabled | BOOL | NONE | |||
timestamp | INT | NONE |
PRODUCT Table | Type | PK | UQ | AI | ONDELETE |
---|---|---|---|---|---|
id | INT | X | X | NONE | |
name | TEXT | X | NONE | ||
category | TEXT | NONE | |||
image | TEXT | NONE | |||
details | TEXT | NONE | |||
privateK | TEXT | X | NONE | ||
publicK | TEXT | X | NONE | ||
apiK | TEXT | X | NONE | ||
lastchecked | INT | NONE |
CLIENT Table | Type | PK | UQ | AI | ONDELETE |
---|---|---|---|---|---|
id | INT | X | X | NONE | |
name | TEXT | NONE | |||
TEXT | NONE | ||||
phone | TEXT | NONE | |||
country | TEXT | NONE | |||
registrydate | INT | NONE |
KEY Table | Type | PK | UQ | AI | ONDELETE |
---|---|---|---|---|---|
id | INT | X | X | NONE | |
productid | INT | FK | CASCADE | ||
clientid | INT | FK | CASCADE | ||
serialkey | TEXT | X | NONE | ||
maxdevices | INT | NONE | |||
devices | INT | NONE | |||
status | INT | NONE | |||
expirydate | INT | NONE |
REGISTRATION Table | Type | PK | UQ | AI | ONDELETE |
---|---|---|---|---|---|
id | INT | X | X | NONE | |
keyid | INT | FK | CASCADE | ||
hardwareid | TEXT | NONE |
CHANGELOG Table | Type | PK | UQ | AI | ONDELETE |
---|---|---|---|---|---|
id | INT | X | X | NONE | |
keyid | INT | FK | CASCADE | ||
userid | INT | FK | SET NULL | ||
timestamp | INT | NONE | |||
action | TEXT | NONE | |||
description | TEXT | NONE |
VALIDATIONLOG Table | Type | PK | UQ | AI | ONDELETE |
---|---|---|---|---|---|
id | INT | X | X | NONE | |
timestamp | INT | NONE | |||
result | TEXT | NONE | |||
type | TEXT | NONE | |||
ipaddress | TEXT | NONE | |||
apiKey | TEXT | NONE | |||
serialKey | TEXT | NONE | |||
hardwareID | TEXT | NONE |
All modifications in SQLAlchemy are based on this model. You are free to use another database, but you will need to change the Flask settings (__init__.py
file).
In order to facilitate the transition between databases, the entire web app connects with the database by using the functions in the databaseAPI.py
file. This means you are free to rewrite these functions, so long the inputs and returns continue to make sense in the context of the overall web app. In any case, the functions either return nothing or they simply return an object whose fields / local variables are identical to each field in the respective table. Some other functions may return specific values. You can see in the table bellow which functions return an object and which don't.
Function name | Return details |
---|---|
generateUser() | None |
obtainUser() | User object (1 record) |
createUser() | None |
changeUserPassword() | None |
toggleUserStatus() | None |
getProduct() | Product object (multiple) |
getProductCount() | Integer |
getDistinctClients() | Integer |
getProductByID | Product object (1 record) |
createProduct() | Product object (1 record) |
editProduct() | None |
getProductThroughAPI() | Product object (1 record) |
resetProductCheck() | None (DEBUG ONLY) |
getKeys() | Key object (multiple) |
getKeysBySerialKey() | Key object (1 record) |
createKey() | ID field of new Key object |
setKeyState() | None |
deleteKey() | None |
resetKey() | None |
getKeyData() | Key object (1 record) |
getKeyStatistics() | 2 Integers |
getKeyAndClient() | Key JOIN Customer object |
updateKeyStatesFromProduct() | None |
applyExpirationState() | Key object |
submitLog() | None |
getKeyLogs() | Changelog object (multiple) |
getUserLogs() | Changelog object (multiple) |
queryLogs() | Changelog object (multiple) |
getRegistration() | Registration object (1 record) |
getKeyHWIDs() | Registration object (multiple) |
deleteRegistrationsOfKey() | None |
deleteRegistrationOfHWID() | None |
addRegistration() | None |
createCustomer() | None |
modifyCustomer() | None |
deleteCustomer() | None |
getCustomer() | Customer object (multiple) |
getCustomerByID() | Customer object (1 record) |
submitValidationLog() | None |
queryValidationLogs() | Validationlog object (multiple) |
queryValidationsStats() | 2 Integers |
Listed bellow, you will see the details of our RESTful API. Each endpoint is listed bellow with their respective details.
Displays the login-form to enter the Dashboard.
Path : /
Method : GET
Authentication required : NO
Parameters : NONE
Response : TEMPLATE_HTML
Displays a tutorial page within the Dashboard.
Path : /tutorial
Method : GET
Authentication required : YES
Parameters : NONE
Response : TEMPLATE_HTML
Displays the dashboard page along with the current statistics of the application.
Path : /dashboard
Method : GET
Authentication required : YES
Parameters : NONE
Response : TEMPLATE_HTML
Displays a webpage with the information of all products in the database, along with their respective details. The administrator is also allowed to create new products or edit existing ones in this webpage.
Path : /products
Method : GET
Authentication required : YES
Parameters : NONE
Response : TEMPLATE_HTML
Displays a webpage with the information of an individual product specified in the Path URL of this endpoint. In it, the administrator can check the API Key of the Product, the Public Key and create Licenses.
Path : /products/id/<productid>
Method : POST
Authentication required : YES
Parameters :
PATH:
productid - The ID of the product we wish to check. Must be a valid ID.
Response : TEMPLATE_HTML
or 404
if the productid is invalid.
Creates a product with the details specified in its body payload. The input must be in JSON format as a dictionary.
Path : /products/create
Method : POST
Authentication required : YES
Parameters :
BODY:
{
'name' : 'Product name',
'category' : 'Product category',
'image' : 'Product image',
'details' : 'Product details'
}
Response : A RESPONSE_FORM
*.
Modifies the data of an existing product with the details specified in its body payload. The input must be in JSON format as a dictionary.
Path : /products/edit
Method : POST
Authentication required : YES
Parameters :
BODY:
{
'id' : 'The ID of the product which data we want to edit'
'name' : 'Product name',
'category' : 'Product category',
'image' : 'Product image',
'details' : 'Product details'
}
Response : A RESPONSE_FORM
*.
Displays a webpage with the all the necessary information about existing Customers. The administrator can also create new Customers, modify their data or even remove existing ones.
Path : /customers
Method : GET
Authentication required : YES
Parameters : NONE
Response : TEMPLATE_HTML
Adds a Customer to the database with the details specified in its body payload. The input must be in JSON format as a dictionary.
Path : /customers/create
Method : POST
Authentication required : YES
Parameters :
BODY:
{
'name' : 'Customer's name',
'email' : 'Customer's email',
'phone' : 'Customer's phone number',
'country' : 'Customer's country'
}
Response : A RESPONSE_FORM
*.
Modifies the data of an existing customer with the new details specified in its body payload. The input must be in JSON format as a dictionary.
Path : /customers/edit/<customerid>
Method : POST
Authentication required : YES
Parameters :
PATH:
customerid (?URL) - The ID of the Customer whose data we wish to modify. Must be a valid ID.
BODY:
{
'name' : 'Customer's name',
'email' : 'Customer's email',
'phone' : 'Customer's phone number',
'country' : 'Customer's country'
}
Response : A RESPONSE_FORM
*.
Deletes the data of an existing customer specified in the URL Path of the endpoint.
Path : /customers/delete/<customerid>
Method : POST
Authentication required : YES
Parameters :
PATH:
customerid - The ID of the Customer we wish to remove.
Response : A RESPONSE_FORM
*.
Creates a license assigned to the Product specified in the URL Path of the endpoint.
Path : /product/<productid>/createlicense
Method : POST
Authentication required : YES
Parameters :
PATH:
productid - The ID of the Product we wish to assign this License to. It must be valid.
BODY:
{
'client' : 'The ID of the customer we will assign this License to',
'maxDevices' : 'The limit of concurrent devices in this License',
'expiryDate' : 'Expiration date for this License'
}
Response : A RESPONSE_FORM
*.
Displays a webpage with the all the necessary information about existing Customers. The administrator can also create new Customers, modify their data or even remove existing ones.
Path : /licenses/<licenseid>
Method : GET
Authentication required : YES
Parameters :
PATH:
licenseid (?URL) - The ID of the License we wish to assign this License to. It must be valid.
Response : TEMPLATE_HTML
Changes the underlying information of the License by either disabling/enabling it, deleting it or completely reseting it.
Path : /licenses/editkeys
Method : POST
Authentication required : YES
Parameters :
BODY:
{
'licenseid' : 'The ID of the customer we will assign this License to',
'action' : 'The action that will be executed. Can be: SWITCHSTATE, DELETE or RESET'
}
Response : A RESPONSE_FORM
*.
Unlinks a device through its Hardware ID from the License.
Path : /licenses/<keyid>/removedevice
Method : POST
Authentication required : YES
Parameters :
PATH:
keyid - The ID of the License we will unlink the Hardware from
BODY:
{
'hardwareID' : 'The ID of the hardware to be unlinked (unique for every device)'
}
Response : A RESPONSE_FORM
*.
Displays a Page where the changelogs will be displayed based on the settings chosen in the form inside.
Path : /logs/changes
Method : GET
Authentication required : YES
Parameters : NONE
Response : A RESPONSE_FORM
*.
Acquires the change logs that fit the criteria sent through the Header.
Path : /logs/changes/query
Method : GET
Authentication required : YES
Parameters :
HEADER:
{
|OPTIONAL| 'adminID' : 'The ID of the administrator whose logs we want to check',
|OPTIONAL| 'datestart' : 'The date that defines the start of our search',
|OPTIONAL| 'dateend' : 'The date that defines the limit of our search',
}
Response : A JSON
dictionary array containing the 'adminid', 'timestamp' and 'description' for each log.
Displays a Page where the validation logs will be displayed based on the settings chosen in the form inside.
Path : /logs/validations
Method : GET
Authentication required : YES
Parameters : NONE
Response : A RESPONSE_FORM
*.
Acquires the validation logs that fit the criteria sent through the Header.
Path : /logs/validations/query
Method : GET
Authentication required : YES
Parameters :
HEADER:
{
|OPTIONAL| 'typeSearch' : 'The type of logs we will be looking for. Successful attempts or failed attempts.',
|OPTIONAL| 'datestart' : 'The date that defines the start of our search',
|OPTIONAL| 'dateend' : 'The date that defines the limit of our search',
}
Response : A JSON
dictionary array containing the Validationlog table rows obtained from the query.
Displays a webpage containing all the admins in the page. Only users flagged as 'Owners' can see this page and they can create admin accounts, disable them or redefine their passwords as they see fit.
Path : /admins
Method : GET
Authentication required : YES (Restricted to Owners)
Parameters : NONE
Response : TEMPLATE_HTML
Creates an admin account that will be allowed to log in and act as an administrator. The data about the account must be sent in JSON format as a dictionary through the payload.
Path : /admins/create
Method : POST
Authentication required : YES
Parameters :
BODY:
{
'email' : 'The admin's email',
'username' : 'The admin's username',
'password' : 'The admin's password'
}
Response : A RESPONSE_FORM
*.
Modifies a specified admin account. This endpoint is, in fact, used only to re-define the password of the specified admin.
Path : /admins/<userid>/edit
Method : POST
Authentication required : YES
Parameters :
PATH:
userid - The admin account's ID that we wish to modify.
BODY:
{
'password' : 'The new password that will be assigned to the admin'
}
Response : A RESPONSE_FORM
*.
Enables or Disables the specified Admin Account based on the current status of the account.
Path : /admins/<userid>/togglestatus
Method : POST
Authentication required : YES
Parameters :
PATH:
userid - The admin account's ID that will have its state changed (disabled or enabled)
Response : A RESPONSE_FORM
*.
Validates a request coming from any external source to decipher whether or not the validation request is valid and that the license indicated is, in fact, genuine. The response follows the same format for all cases.
Path : /api/v1/validate
Method : POST
Authentication required : NO
Parameters :
BODY:
{
'apiKey' : 'The API Key of the Product that the customer's license belongs to. Used to locate the product'
'payload' : 'A PublicKey-encrypted message that containts the HardwareID and the Serial Key of the License'
}
Response : A JSON
dictionary array containing four fields. It has a code indicating whether or not the validation succeeded (if the code starts with ERR_
then the validation failed). It also has a description elaborating the reason why it failed.
*RESPONSE_FORM
- For every single endpoint above, this type of JSON dictionary response carries a CODE and a MESSAGE. The CODE is used by the script to know if the request succeeded. If it didn't, then the javascript will show the server-generated message to the client. Example:
{
'HttpCode' : str(HTTPCode),
'Message' : str(Message),
'Code' : str(ResponseCode),
'SerialKey' : str(key),
'HardwareID' : str(hwid),
'ExpirationDate' : int( -1 ) || int( expirationDate )
}
SLM is still a work in progress and is completely available for improvement. You can help out by:
- Suggesting changes to the code by submitting issues
- Making your own changes and requesting merge pulls
- Creating your own version based on the current state of this project
- Patching critical security issues
You are also encouraged to write unit tests for new functionalities or even existing ones. Although one of the achievements of this application is to make a clear and readable interface, ensuring security is still the top priority in the issue list. Make sure to tag any security issues clearly.
- Linter:
pylint
- Formatter:
autopep8