ARGO Accounting
This project uses Quarkus, the Supersonic Subatomic Java Framework.
- Java 11+
- Apache Maven 3.8.1+
- Docker (for dev mode)
Quarkus supports the automatic provisioning of unconfigured services in development and test mode. They refer to this capability as Dev Services. From a developer’s perspective this means that if you include an extension and don’t configure it then Quarkus will automatically start the relevant service (usually using Testcontainers behind the scenes) and wire up your application to use this service.
You can run your application in dev mode that enables live coding using:
mvn clean compile quarkus:dev
The application can be packaged using:
mvn clean package
It produces the quarkus-run.jar
file in the target/quarkus-app/
directory.
Be aware that it’s not an über-jar as the dependencies are copied into the target/quarkus-app/lib/
directory.
The application is now runnable using java -jar target/quarkus-app/quarkus-run.jar
.
If you want to build an über-jar, execute the following command:
mvn clean package -Dquarkus.package.type=uber-jar
The application, packaged as an über-jar, is now runnable using java -jar target/*-runner.jar
.
You can create a native executable using:
mvn clean package -Pnative
Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
mvn clean package -Pnative -Dquarkus.native.container-build=true
You can then execute your native executable with: ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner
If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling.
When running the production version of the application, the MongoDB connection needs to be configured as normal by setting the connection string in quarkus.mongodb.connection-string
.
If you want to continue use Dev Services we recommend that you use the %prod.
profile to define your MongoDB settings. What that means practically, is that Quarkus
will automatically start a MongoDB container when running tests or dev-mode, and automatically configure the connection.
The possible values of unit_type
and metric_type
are defined in two separate files, unit_type.conf
and metric_type.conf
respectively.
The application reads those files and offers to the clients the available values by the respective GET requests.
You can set the path to those files by filling in the following attributes in the application.properties
:
unit.types.file
metric.types.file
Finally, under the folder /files
, you can find samples of those files.
To access Accounting System API resources, you have to be authenticated by GRNET's keycloak. These resources are protected and can only be accessed if a client is sending a bearer token along with the request, which must be valid and trusted by the Accounting System API.
The GRNET's keycloak offers various Identity Providers where the authentication process can be performed.
Once you log in to your preferable Identity Provider, you obtain an access token. Using this token, you can access the available API operations.
When passing in the access token in an HTTP header, you should make a request like the following:
curl http://localhost:8080/accounting-system/metric-definition
-H "Authorization: Bearer {token}"
There is an ancillary web page at {accounting_system_host}
where you can identify yourself. This page is responsible for :
- redirecting a user to Keycloak's login page in order to be authenticated
- displaying the obtained token
Generally, the ancillary web page applies a client-side JavaScript library that can be used to secure HTML5/JavaScript applications. More details you can find here. If you want to secure your application, you can apply any of the available Keycloak adapters.
The Accounting System API also needs to authenticate services. For this scenario, typical authentication schemes like username + password don't make sense. Instead, the services use the Client Credentials Flow , in which they pass along their Client ID and Client Secret to authenticate themselves and get an access token.
The Client ID and Client Secret are generated by Keycloak and provided by GRNET. Once the service acquires them, it can obtain a token by executing the following HTTP request:
curl --location --request POST 'https://keycloak/auth/realms/einfra/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=<CLIENT_ID>' \
--data-urlencode 'client_secret=<CLIENT_SECRET>' \
--data-urlencode 'grant_type=client_credentials'
Quarkus starts a Keycloak container for both the dev and/or test modes and initializes them by registering the existing Keycloak realm or creating a new realm with the client and users for you to start developing the Accounting System application secured by Keycloak immediately. More details about how to obtain access tokens in dev mode you can find here.
If you want to integrate with external keycloak, you have to remove the prefix %prod.
from %prod.quarkus.oidc.auth-server-url
.
Then you have to configure the following variables in application.properties
:
quarkus.oidc.auth-server-url={The base URL of the Keycloak server. The base URL will be in the following format: https://host:port/auth/realms/{realm}.
quarkus.oidc.client-id={Specifies an alpha-numeric string that will be used as the client identifier for OIDC requests}
quarkus.oidc.credentials.secret={Client secret which is used for a client_secret_basic authentication method}
%dev.key.to.retrieve.roles.from.access.token=roles
To use a UI Page to obtain an access token from the external keycloak, which will be accessible at {accounting_system_host}, you have to configure the following variables in application.properties
:
keycloak.server.url={The base URL of Keycloak server. Make sure the base URL is in the following format: https://host:port/auth}
keycloak.server.realm={The name of the Keycloak realm}
keycloak.server.client.id={Specifies an alpha-numeric string that will be used as the client identifier for OIDC requests}
The keycloak.server.url
, keycloak.server.realm
and keycloak.server.client.id
are used in order to feed the keycloak.html template.
There also is an endpoint which is responsible for rendering that html page.
Collection-level permissions control services’ access to the collection. Collection permissions are configured using roles, and each collection contains a set of roles that allows services to create, read, update and delete entities of that collection. We assign permissions to services based on their roles. A role is a collection of permissions that you can apply to services. We can assign one or more roles to each service and one or more permissions to each role.
To generate a role, we have to define the available API operations and access types. By combining operations and access types, you can generate permissions and then you can assign these permissions to each role.
The available API operations are:
Operation | Description |
---|---|
Create | Can create new entities within a collection |
Update | Can update existing entities within a collection |
Delete | Can delete existing entities within a collection |
Read | Can fetch existing entities within a collection |
Acl | Can specify which services are granted access to particular entities within a collection |
The following table illustrates the available access types:
Access Type | Description |
---|---|
Always | Services are always able to perform this operation |
Never | Services are never able to perform this operation |
Entity | Services have only access to entities that they have created |
If the service has more than one role :
- If the access types are Always and Entity, the service will have Always access, because this access type is the most permissive and will override the others. In other words, the most permissive access type takes precedence.
- Never always takes precedence over any other access type.
The available Accounting System API collections are:
Collections |
---|
MetricDefinition |
Metric |
Role |
Provider |
Based on the above, we can generate some indicative roles:
Role | Create | Update | Delete | Read | Acl | Collection |
---|---|---|---|---|---|---|
metric_definition_admin | Always | Always | Always | Always | Always | MetricDefinition |
metric_inspector | Always | Metric | ||||
metric_creator | Always | Entity | Entity | Entity | Entity | Metric |
role_editor | Always | Always | Never | Always | Role |
Notes:
- The role name should be unique.
- The blank value is converted to "Never" value
Consequently:
- metric_definition_admin can create new Metric Definitions, as well as update, delete and read any entity in the collection. Can also specify which services will be granted access to any entity within a Metric Collection. In other words, metric_definition_admin will always be able to perform any operation in the Metric Definition collection.
- metric_inspector can read any entity in the Metric collection, but cannot create new ones or update, delete any Metrics.
- metric_creator can create new Metrics, but can update, delete or read only its Metrics. It can also specify which services will have access to the entities it has created. Finally, it can also manage Metrics that have been explicitly declared through the ACL process.
- role_editor can create new Roles, as well as update, and read any entity in the Role collection but cannot delete any entity in it.
It is possible to create and manage new roles using the API. It should be noted that only the system_admin role, which is initialized in the API, can create and manage roles through the API. Consequently, it is the only role that can grant access to other roles.
We can create the aforementioned roles by executing the following requests:
- metric_definition_admin
POST 'https://host/accounting-system/roles'
{
"name" : "metric_definition_admin",
"collection_permission_list":[
{
"collection": "MetricDefinition",
"permissions" :[
{
"operation" : "CREATE",
"access_type" : "ALWAYS"
},
{
"operation" : "UPDATE",
"access_type" : "ALWAYS"
},
{
"operation" : "DELETE",
"access_type" : "ALWAYS"
},
{
"operation" : "READ",
"access_type" : "ALWAYS"
},
{
"operation" : "ACL",
"access_type" : "ALWAYS"
},
]
},
]
}
- metric_inspector
POST 'https://host/accounting-system/roles'
{
"name" : "metric_inspector",
"collection_permission_list":[
{
"collection": "Metric",
"permissions" :[
{
"operation" : "READ",
"access_type" : "ALWAYS"
}
]
},
]
}
- metric_creator
POST 'https://host/accounting-system/roles'
{
"name" : "metric_creator",
"collection_permission_list":[
{
"collection": "Metric",
"permissions" :[
{
"operation" : "CREATE",
"access_type" : "ALWAYS"
},
{
"operation" : "UPDATE",
"access_type" : "ENTITY"
},
{
"operation" : "DELETE",
"access_type" : "ENTITY"
},
{
"operation" : "READ",
"access_type" : "ENTITY"
},
{
"operation" : "ACL",
"access_type" : "ENTITY"
}
]
},
]
}
- role_editor
POST 'https://host/accounting-system/roles'
{
"name" : "role_editor",
"collection_permission_list":[
{
"collection": "Role",
"permissions" :[
{
"operation" : "CREATE",
"access_type" : "ALWAYS"
},
{
"operation" : "UPDATE",
"access_type" : "ALWAYS"
},
{
"operation" : "DELETE",
"access_type" : "NEVER"
},
{
"operation" : "READ",
"access_type" : "ALWAYS"
}
]
},
]
}
You can also create a more generic role. For example, the editor can create new Roles, Metrics, and Metric Definitions, as well as update, and read any entity in those collections. But cannot delete any entity:
- editor
POST 'https://host/accounting-system/roles'
{
"name" : "editor",
"collection_permission_list":[
{
"collection": "Role",
"permissions" :[
{
"operation" : "CREATE",
"access_type" : "ALWAYS"
},
{
"operation" : "UPDATE",
"access_type" : "ALWAYS"
},
{
"operation" : "READ",
"access_type" : "ALWAYS"
}
]
},
{
"collection": "Metric",
"permissions" :[
{
"operation" : "CREATE",
"access_type" : "ALWAYS"
},
{
"operation" : "UPDATE",
"access_type" : "ALWAYS"
},
{
"operation" : "READ",
"access_type" : "ALWAYS"
}
]
},
{
"collection": "MetricDefinition",
"permissions" :[
{
"operation" : "CREATE",
"access_type" : "ALWAYS"
},
{
"operation" : "UPDATE",
"access_type" : "ALWAYS"
},
{
"operation" : "READ",
"access_type" : "ALWAYS"
}
]
}
]
}
The generated roles can be assigned to different users or services via Keycloak.
To deploy the API in a machine :
- First, you have to set up a mongo database (We are currently using
4.0.28
, but other versions may be compatible) - Second, you have to put the
unit_type.conf
andmetric_type.conf
in the machine filesystem - Third, there must be an über-jar version of the API on this machine
Before running the über-jar, you have to export the following variables:
- export QUARKUS_MONGODB_CONNECTION_STRING=mongodb://{host}:{port}
- export UNIT_TYPES_FILE={filesystem path to unit_type.conf}
- export METRIC_TYPES_FILE={filesystem path to metric_type.conf}
- export SERVER_URL={The proxy server URL that acts on behalf of the Accounting System}
- export QUARKUS_OIDC_AUTH_SERVER_URL={The base URL of the OpenID Connect (OIDC) server. Note if you work with Keycloak OIDC server, make sure the base URL is in the following format: https://host:port/auth/realms/{realm} where {realm} has to be replaced by the name of the Keycloak realm}
- export QUARKUS_OIDC_CLIENT_ID={Specifies an alpha-numeric string that will be used as the client identifier for OIDC requests}
- export QUARKUS_OIDC_CREDENTIALS_SECRET={Client secret which is used for a client_secret_basic authentication method}
- export KEYCLOAK_SERVER_URL={The base URL of Keycloak server. Make sure the base URL is in the following format: https://host:port/auth}
- export KEYCLOAK_SERVER_REALM={The name of the Keycloak realm}
- export KEYCLOAK_SERVER_CLIENT_ID={Specifies an alpha-numeric string that will be used as the client identifier for OIDC requests}
Once the variables above have been exported, you should execute the following command:
java -jar *-runner.jar
Once the application is started, you can make a request to the /open-api
endpoint:
curl {accounting_system_host}/open-api
Swagger UI is accessible at /swagger-ui
endpoint:
{accounting_system_host}/swagger-ui