Compare commits
43 Commits
a0468d2e6a
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 86df5b9bc7 | |||
| 61bea5fb6e | |||
| c835a9deb0 | |||
| b862a3f686 | |||
| 5139f30e86 | |||
| e964c74f96 | |||
| bc00515c1b | |||
| 60f9aa05a0 | |||
| 4705649ae8 | |||
| 5acaffc224 | |||
| 0a1b901b12 | |||
| 440c13cc49 | |||
| 9b9557105c | |||
| af360d7793 | |||
| 452e6189e4 | |||
| 7e4fb66fc6 | |||
| 213f560c4a | |||
| 8449578424 | |||
| 1adcc04efc | |||
| 421911e41b | |||
| 0e5de99f55 | |||
| 0dae53017d | |||
| 934937feb0 | |||
| dcb6850306 | |||
| ebe8e5d574 | |||
| f264f90f3b | |||
| ba820e428e | |||
| cd11b389e5 | |||
| 6a91eab764 | |||
| 5b8280cbc3 | |||
| 1481c84770 | |||
| b3836639bf | |||
| 6f1fb56670 | |||
| a8aa61336f | |||
| 8ef8bf5d92 | |||
| d684931bc5 | |||
| 3cf5c83264 | |||
| ebeaaa2629 | |||
| b84dbc9a56 | |||
| 1817d02c31 | |||
| 4afe00e1f8 | |||
| 60f5450f47 | |||
| d6dc33d9b1 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
/target/
|
||||
/config/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
### STS ###
|
||||
@@ -28,3 +27,10 @@
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
/cygnus-onprem-app/target
|
||||
/cygnus-cloud-client/target
|
||||
/cygnus-cloud-service/target
|
||||
/cygnus-installer/src/target
|
||||
/cygnus-installer/target
|
||||
/cygnus-onprem-db/target
|
||||
/cygnus-lib/target
|
||||
|
||||
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@@ -30,7 +30,13 @@
|
||||
"CYGNUS_ACCESS_TOKEN_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/access-token-public.pem",
|
||||
"CYGNUS_LOGIN_KEY_ID": "cygnus-login-2026-01",
|
||||
"CYGNUS_LOGIN_PRIVATE_KEY": "file:${workspaceFolder}/config/keys/login-private.pem",
|
||||
"SPRING_CONFIG_ADDITIONAL_LOCATION": "file:${workspaceFolder}/config/clients.yml"
|
||||
"CYGNUS_MAIL_HOST": "smtp.gmail.com",
|
||||
"CYGNUS_MAIL_PORT": "587",
|
||||
"CYGNUS_MAIL_USERNAME": "technobeesolutions@gmail.com",
|
||||
"CYGNUS_MAIL_PASSWORD": "lrideibfakickldg",
|
||||
"CYGNUS_MAIL_SMTP_AUTH": "true",
|
||||
"CYGNUS_MAIL_STARTTLS": "true",
|
||||
"CYGNUS_REGISTRATION_EMAIL_FROM": "technobeesolutions@gmail.com"
|
||||
},
|
||||
"shortenCommandLine": "argfile"
|
||||
},
|
||||
@@ -53,7 +59,9 @@
|
||||
"REDIS_HOST": "103.125.129.116",
|
||||
"REDIS_PORT": "7901",
|
||||
"REDIS_PASSWORD": "M@triXR3d1s@6202",
|
||||
"REDIS_DATABASE": "1",
|
||||
"REDIS_SSL": "false",
|
||||
"CYGNUS_QUERY_CACHE_ENABLED": "false",
|
||||
"CYGNUS_CLOUD_BASE_URL": "http://localhost:8090",
|
||||
"CYGNUS_TOKEN_URL": "http://localhost:8090/oauth2/token",
|
||||
"CYGNUS_CLIENT_ID": "matrix",
|
||||
@@ -61,6 +69,8 @@
|
||||
"CYGNUS_CLIENT_ASSERTION": "file:${workspaceFolder}/config/clients/matrix/matrix-matrix-delhi-cygnus-01-assertion.jwt",
|
||||
"CYGNUS_LOGIN_KEY_ID": "cygnus-login-2026-01",
|
||||
"CYGNUS_LOGIN_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/login-public.pem",
|
||||
"CYGNUS_PAYLOAD_PRIVATE_KEY": "file:${workspaceFolder}/config/keys/case-save-private.pem",
|
||||
"CYGNUS_PAYLOAD_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/case-save-public.pem",
|
||||
"CYGNUS_CLOUD_REQUEST_TIMEOUT": "PT10S",
|
||||
"CYGNUS_TOKEN_REFRESH_SKEW": "PT30S"
|
||||
},
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.rf8bViJPb5bRJHFamVoCftE0TLA1Gz7JyGnfcjtKEx3KiUL4SSbgYSUQlsKhCt8VJMWE--UpgbkM0Bv3pFVsMF_uh693MhFstEy_A6S3MODjoUV_bMLa_Zr1yH2Jwzud4LSyw-ctJ7G-vO8S_I6YML0HV5kNbobdFmMfXFLGKssVmM_Km0m57ZgSjj3zRii0cKpmdwwSbWoJC2y519TpKhIja0OZAoN1oB3IVKvERlFYCQYUpQmcRkzULGrlSoLIm9iqbRX4qMGhGISwVS6JdVtn9WjpSjF3xkUxN_QsVuYKYFL_kEKWSNepIcfoip7Ag9Qw_S1RZA_KO908MgrzGRbs2UIpxY9sAM8yZ-k1NTk0swV836s5pdDIP3PHovcd6iR8mqWBPt7mq7ES98vNeCZxqPLB_b1JuNfKsbLPdRIvEk2DTjZHeEi-yA1yl5uMi_1f4la6mgy65u_jLL_Ow28uZtj-8yQ3dR4lF3K7tkxQaB4LQiH5nl6lXEndpoPe.Cpu6VoHf2ptCue_L.uH-Pq1WZN-x_c71Iyx8X1QJa1JZRp9sYta9iUOmQ4W8x4Qh2wYm-_tpr8FEw56MLvIb8IH_gHVVLAJgXTmCZhyo26vJlPCtnMMPYFDrPRzTJv9Ewg7RMpsAh_kXFFK4UJ6Tgt_r7Tg_bnVOTKXodUjl3wVdMowZRX_ua-gKwLU-2LDWdqIRmuFDSOkMb1sFBZLPyjWHg_rThs-RwVa4YJdDi74s-L0WX0xRPXzeovu1-xvOgZ0xYf0ScbsFQgvK-JwAxOWsB1WKeHRqnnx5-apK6qtzhpiJ30c7ukPlrg2nieuJvkXOVuKBKXLVRLSViOH4HU76DD96btDUA0Cg2kEURpepucX0VWPyB7K3xb9v1AT42TUkW9wZzdmIs9saRo-ItuWzlCezvapLIsbqmYCs_rW2J3KgqXrUyy2mptn32sqaqQZnlNVu4wwboPCCrbl2MG5CYlYKrwaOHNg611j8sCDkIptm_UmPFnOnzvQYwURuDSgzswTIV5bTd_58yxag2yWwiMdGLMDbxxSUPQ6a4EbuDkqwmGBTSLiG7SaSFchSg6o9EflqK6k-u_rlixCxmfg1A6sa7nloW17dOxDzGKtmn9lBoaskL97nNrhP6ptftak7j4TIZulz1FUOSVoR8kXK8RNQMj-b6rg_MdSO9Ecq5hZsza1SzjkzKZvV-tpP6Wl6-rszCH6g3DIQAWN5aWdUkwuya1HqDDyVPbUOVQPsp4nhVC24QmnlbcqFwQdoCrikYKyX3Snc0H6_K4z9c6nURAdj6THwHmA9BfHtJniI_rEIvkmBk3yndi-H5eAjtxIoYSFQrSTWEFhgvk6bAKoR_xb_cHgzKOxYLBiO_FifkjkFDOlkVBuYnZEp3oRePDKgMDfL5Oxjuie4u-8yrH52Cd32yQzbvGplaXLLeEDf3i71dcz4LkO5DknScWG3G_SbKBpjlse-GKe3XYqRXeRIYLpyiX1MXph0b-Q.dvHU75ZR8wJFxoSacfkthw
|
||||
40
config/clients/matrix/matrix-signing-private.pem
Normal file
40
config/clients/matrix/matrix-signing-private.pem
Normal file
@@ -0,0 +1,40 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
M/D4ZjXsfrW//BMF6+w2Bh4=
|
||||
-----END PRIVATE KEY-----
|
||||
11
config/clients/matrix/matrix-signing-public.pem
Normal file
11
config/clients/matrix/matrix-signing-public.pem
Normal file
@@ -0,0 +1,11 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAloJtGMOLcTNWe1IgdrT8
|
||||
uYucI1Vs02WjyUPZ7Po51iC67QmGdG0InzlBJYdkYtTz8eda+qArZMNGB0ChFs/7
|
||||
xKPKASkMvVcrm4IYbth0DEIkXWAL7YXsWd8qdS+KsnxuC6TqvuA9Xq/PX4RmtAq4
|
||||
bl0/1T2HZjJs53/b73l5NehcseZU129w+5lBqDwW5xh+1VnXNbc96za7BFfQSnfg
|
||||
9WwaWSpo8Y57XATnV7s46yT8gNKM29INHJXs4aIrQ9tcLnTbDz9nhGceNgec3AhO
|
||||
zc4TyCPugUiG9WD4ehsQr+7Cp30RqT5l9ZPvoBTpAdHLZ1r/jB+qY6+WkY6EsOJW
|
||||
2xg9bUYoHXjMiFdi8OLXC4NTAPUquFOaIMfjxGaYQc8NoDMFiq3qdTWi2ndCOoxX
|
||||
yFxYRP5pYs/oA46UAHZsCHaspjvl9jfR/abCALBtsFLwpLA9qqBws5ojXFH//UoX
|
||||
E7dXnJziCFQNyJOhXtIhbQhtb3wJc6WSLtDrRpFtaJudAgMBAAE=
|
||||
-----END PUBLIC KEY-----
|
||||
40
config/keys/access-token-private.pem
Normal file
40
config/keys/access-token-private.pem
Normal file
@@ -0,0 +1,40 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
m/lDtrY4EpMy9FSQD1zb+qE=
|
||||
-----END PRIVATE KEY-----
|
||||
11
config/keys/access-token-public.pem
Normal file
11
config/keys/access-token-public.pem
Normal file
@@ -0,0 +1,11 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvyfuM67QxEonEvC2Vf21
|
||||
P+fxo406Z4C+zNzxH/8tCPhMH94Sklt10WXqSfQFfZbhJPVYoZL8m0NDI7ozI7lI
|
||||
MPAhAVfcFOzO6VqpFGrzMgMqmnWLrxRsL3y1rjB3LnE84W+ATcS/mlk6rwbCfrSo
|
||||
RRlY3i79ol8h36jhhaHT2/iGGO3uvqAQK334lQEOlqr+YL4Kkr/GMOm86bE4PcZS
|
||||
JgCTAnlCQC7TUu6jGWuocmI1nNwLXPjQvXgc+HKtJEqFGf+a0fty9fLgaR2yIzlM
|
||||
9i31r3kjeKzqVe2DeLTbttd8bVWU4/KmGg8rEchtwC8wR4inuDUjwJ3FwmdBrvDp
|
||||
Ga1AM8yofdbs73Lv2EgkvRLvlgaWiX2P6KdlKGHeaDj9v0YWJbiFM4rc9y5hD3lG
|
||||
2unhpvP/49OFXVBT2/pm86LmxSlwsun3G2LKNOXI646jhdVHQkIVcBiHTHYQOe9/
|
||||
xLqx3ZiEM7witbzuJnxIkbj/8OFgH+WDUmTQVXtbz7vdAgMBAAE=
|
||||
-----END PUBLIC KEY-----
|
||||
40
config/keys/assertion-decryption-private.pem
Normal file
40
config/keys/assertion-decryption-private.pem
Normal file
@@ -0,0 +1,40 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
RqsZZ1cLu0TCOoNBkcSr79a6
|
||||
-----END PRIVATE KEY-----
|
||||
11
config/keys/assertion-decryption-public.pem
Normal file
11
config/keys/assertion-decryption-public.pem
Normal file
@@ -0,0 +1,11 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAtZc750+cxwDcG/lvpEHm
|
||||
3fxFgp5X5df90s51R4ShKpV6dmDRYXnVil9GmXrJf/aGAqrsf6ZUvZWJrAJ2OO2X
|
||||
Wt8tcqajnaWdM33BrH/QztTJkoGHZM1xLY5EpRaWo1LaG2gNGsaxG0ON9T1emKi7
|
||||
XDEcI9Lmc/mhP6SuuUnvGwq1Z433vqGodorkU+T2jCfflhwfenvBcOk4GJZoJFlY
|
||||
sjy9Dtlha70MJpdVFWffxs4UBhhkjckb7GbDTVxa3xvz1NEbkY2+oh/tJmuIuupA
|
||||
7A6TnfYP1YPXPMRQFtSPaKMOeKW5J5IorvgQKr4C74HR9iBojJN2oJ6/BjfAaz7P
|
||||
8kywLj2uo9gJemOm1wz8HZyZsz482GHa0wqpUcFfOIr0oTMiJtvLn/FSYVb5xvNg
|
||||
3xWiGPMju32Z49XTX7VxTDsIjGx5wtDd4G8ILS3yUlKw0yufHacmy8Fn+Plk+Xtz
|
||||
Bbz6rq4frV93/3mfVMPGgBDx8mLjVJovPBnXKBSX+SdbAgMBAAE=
|
||||
-----END PUBLIC KEY-----
|
||||
40
config/keys/case-save-private.pem
Normal file
40
config/keys/case-save-private.pem
Normal file
@@ -0,0 +1,40 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
5PO2cnRk24OsUqAswTBT2qjd
|
||||
-----END PRIVATE KEY-----
|
||||
11
config/keys/case-save-public.pem
Normal file
11
config/keys/case-save-public.pem
Normal file
@@ -0,0 +1,11 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAzpOwECKVW0KdZwlVJtW7
|
||||
tkqqa1wY1J+HhuFf/kbjmJv8fwFcGj+uyy2s5cXzgckNHz3Cv4Ki8VnQakbn83KR
|
||||
TNIuMvYgwnaNY35KvVKU5R8KPJ7wV8yEUyhRyjtauaY9TaVUH0MStD3izcZ7Pfej
|
||||
XJvxRtLjQ9SX/K+joHs3mkWyWDNYm9u6ecLBoPxW6BpAXiUOWnSpc0IO2njGbvnY
|
||||
I7zs5tYdFjVtDTf4TOSIDXeCXc7nNUhf/yQVAEKvGmr64SihZc+11T5AXJy1eVEj
|
||||
Fwj7hWqcZxhgukacOoU8oUORO0O0H1HIS8ICXHBgKL17Jq+furEInlU4Pfp1tkA7
|
||||
dYh9nXQ91+hmRtTSXA1BpzWdIzC3fQqnwTsD6cyIDsPdRB9iSJTywFPbGywMIbh4
|
||||
wIpUOS3s83DBtpxj2qHw7zc5Q99BP4Dd38kl9ejfdYuFYRmbzrq+BZR7ic5/LM7+
|
||||
8iExYGpz3sFCr6qJAlCDK4th6TCl2ZjzgpTOF3pnC1I7AgMBAAE=
|
||||
-----END PUBLIC KEY-----
|
||||
40
config/keys/login-private.pem
Normal file
40
config/keys/login-private.pem
Normal file
@@ -0,0 +1,40 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
VkpSw8QqBfhHSViEh12BQZa2
|
||||
-----END PRIVATE KEY-----
|
||||
11
config/keys/login-public.pem
Normal file
11
config/keys/login-public.pem
Normal file
@@ -0,0 +1,11 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA2g0IbNxsNIZ6fKaGTHTQ
|
||||
pUFfgJVq3x0SZ458xAQDL0l1PZ+lnMtvS3/ZZpbF99bLuqyzUuHwi+1BJ/Nled61
|
||||
oK8RdJKfUPiQUshXfvWT4/sLsYO6H+v2qw2jBv6kx3h+teDiuHMbw6ikUECr5u1k
|
||||
qkjb+ls3aijtWplmN25HRhwFB/l6/0xAhEQNA36EoNb+ufsDR/2aRQYGQ6qegynp
|
||||
8bUt7JDf3hW1y76UCKlKteFfIOkp4zwY0YYq/NewgKlze08LBrX+zy5xr5Vn8nE0
|
||||
ZtWY8vbNC33Sf6g5D1ZlA+HvquplAWS/rPymj+IN79FeXNegmyCxF/ZafsneaUBn
|
||||
GpSgXYbyASpA+OkqUqJcBEeTI4rfRZPiyx3ztaibg+rgOEM/DUV5le2WyujnsQ75
|
||||
IG4SrVu/XTmF08MN8/hD1+ydBVnhVa7bE4WctqgA1T/5BP7ivAB03a1BJHpKFRh/
|
||||
Q/pO5ONIK8lNGG6aImLPmtJijEvOL0VsUdp+oO87VnTFAgMBAAE=
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -29,18 +29,24 @@ It is valid for one year; the access token obtained with it is short-lived.
|
||||
|
||||
For local development, the repository setup script automates prerequisite
|
||||
checks, the full Maven verification, directory creation, all three cloud key
|
||||
pairs, the installation key pair, `config/clients.yml`, and the encrypted
|
||||
machine assertion:
|
||||
pairs, the installation key pair, database-backed tenant/install registration,
|
||||
an initial license, and the encrypted machine assertion:
|
||||
|
||||
```bash
|
||||
./scripts/setup-local-communication.sh
|
||||
```
|
||||
|
||||
The script interactively asks for the customer identifier, installation
|
||||
identifier, cloud URL, and whether to run the full verification. Customer and
|
||||
installation identifiers cannot contain spaces; the customer identifier is
|
||||
used for its directory and signing-key filenames. New customers are appended
|
||||
to `config/clients.yml` without replacing existing customers.
|
||||
The script interactively asks for the customer name and slug, installation
|
||||
identifier, cloud URL, database connection, license package/type/duration, and
|
||||
whether to run the full verification. Customer and installation identifiers
|
||||
cannot contain spaces. The slug is the stable tenant key and is used for its
|
||||
directory and signing-key filenames.
|
||||
|
||||
The client account, installation public key, allowed scopes, and license are
|
||||
upserted into PostgreSQL (`identity.client_account`,
|
||||
`identity.client_installation`, and `identity.client_license`). The cloud
|
||||
service resolves this registration dynamically through Redis with PostgreSQL
|
||||
fallback, so adding another customer does not require a cloud restart.
|
||||
|
||||
It preserves existing private keys and assertions. Set
|
||||
`CYGNUS_SETUP_FORCE_ASSERTION=true` only when the assertion needs to be
|
||||
@@ -65,6 +71,6 @@ mvn -pl cygnus-cloud-client exec:java \
|
||||
client-signing-private.pem cloud-assertion-public.pem machine-assertion.jwt"
|
||||
```
|
||||
|
||||
Copy only `client-signing-public.pem` into that customer's cloud-side client
|
||||
configuration. Keep the private key and generated assertion on the on-premises
|
||||
server with owner-only filesystem permissions.
|
||||
The setup script stores `client-signing-public.pem` in the installation record
|
||||
used by the cloud. Keep the private key and generated assertion only on the
|
||||
on-premises server with owner-only filesystem permissions.
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.cygnus.client;
|
||||
|
||||
import com.cygnus.client.model.CloudIdentitySession;
|
||||
import com.cygnus.client.model.CloudDataItem;
|
||||
import com.cygnus.client.model.CloudDataRequest;
|
||||
import com.cygnus.client.model.CloudQueryResponse;
|
||||
import com.cygnus.client.model.LoginPayload;
|
||||
import com.cygnus.client.security.LoginEnvelopeEncryptor;
|
||||
import com.cygnus.client.security.MachineTokenProvider;
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -50,4 +55,40 @@ public class CloudIdentityClient {
|
||||
.bodyToMono(CloudIdentitySession.class))
|
||||
.timeout(properties.requestTimeout());
|
||||
}
|
||||
|
||||
public Mono<CloudQueryResponse> fetchQuery(int queryId) {
|
||||
return tokenProvider.accessToken()
|
||||
.flatMap(token -> webClient.get()
|
||||
.uri(properties.baseUri().resolve("/api/v1/queries/" + queryId))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(CloudQueryResponse.class))
|
||||
.timeout(properties.requestTimeout());
|
||||
}
|
||||
|
||||
public Mono<CloudQueryResponse> fetchQuery(String queryKey) {
|
||||
return tokenProvider.accessToken()
|
||||
.flatMap(token -> webClient.get()
|
||||
.uri(properties.baseUri().resolve("/api/v1/queries/key/" + queryKey))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(CloudQueryResponse.class))
|
||||
.timeout(properties.requestTimeout());
|
||||
}
|
||||
|
||||
public Mono<List<CloudDataItem>> fetchData(String scope, Map<String, Object> data) {
|
||||
return tokenProvider.accessToken()
|
||||
.flatMap(token -> webClient.post()
|
||||
.uri(properties.baseUri().resolve("/api/v1/platform/data"))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(new CloudDataRequest(scope, Map.copyOf(data)))
|
||||
.retrieve()
|
||||
.bodyToFlux(CloudDataItem.class)
|
||||
.collectList())
|
||||
.timeout(properties.requestTimeout());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.cygnus.client.model;
|
||||
|
||||
public record CloudDataItem(Object value, String label, String group) {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.cygnus.client.model;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public record CloudDataRequest(String scope, Map<String, Object> data) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.cygnus.client.model;
|
||||
|
||||
public record CloudQueryResponse(int queryId, String query) {
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
artifactId=cygnus-cloud-client
|
||||
groupId=com.cygnus
|
||||
version=1.0.0-SNAPSHOT
|
||||
@@ -1,14 +0,0 @@
|
||||
com/cygnus/client/security/MachineTokenProvider.class
|
||||
com/cygnus/client/security/OAuthMachineTokenProvider$TokenResponse.class
|
||||
com/cygnus/client/provisioning/MachineAssertionGenerator.class
|
||||
com/cygnus/client/CloudClientProperties.class
|
||||
com/cygnus/client/security/LoginEnvelopeEncryptor.class
|
||||
com/cygnus/client/model/LoginPayload.class
|
||||
com/cygnus/client/model/CloudIdentitySession.class
|
||||
com/cygnus/client/security/OAuthMachineTokenProvider.class
|
||||
com/cygnus/client/security/OAuthMachineTokenProvider$CachedToken.class
|
||||
com/cygnus/client/CloudClientFactory.class
|
||||
com/cygnus/client/model/CloudMenuItem.class
|
||||
com/cygnus/client/model/EncryptedLoginRequest.class
|
||||
com/cygnus/client/security/CloudClientException.class
|
||||
com/cygnus/client/CloudIdentityClient.class
|
||||
@@ -1,12 +0,0 @@
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/CloudClientFactory.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/CloudClientProperties.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/CloudIdentityClient.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/model/CloudIdentitySession.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/model/CloudMenuItem.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/model/EncryptedLoginRequest.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/model/LoginPayload.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/provisioning/MachineAssertionGenerator.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/security/CloudClientException.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/security/LoginEnvelopeEncryptor.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/security/MachineTokenProvider.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/main/java/com/cygnus/client/security/OAuthMachineTokenProvider.java
|
||||
@@ -1 +0,0 @@
|
||||
com/cygnus/client/CloudClientPropertiesTest.class
|
||||
@@ -1 +0,0 @@
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-client/src/test/java/com/cygnus/client/CloudClientPropertiesTest.java
|
||||
@@ -1,64 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report.xsd" version="3.0.2" name="com.cygnus.client.CloudClientPropertiesTest" time="0.016" tests="2" errors="0" skipped="0" failures="0">
|
||||
<properties>
|
||||
<property name="java.specification.version" value="21"/>
|
||||
<property name="sun.jnu.encoding" value="UTF-8"/>
|
||||
<property name="java.class.path" value="/Users/maddy/Projects/matrix/cygnus-cloud-client/target/test-classes:/Users/maddy/Projects/matrix/cygnus-cloud-client/target/classes:/Users/maddy/.m2/repository/org/springframework/spring-webflux/6.2.19/spring-webflux-6.2.19.jar:/Users/maddy/.m2/repository/org/springframework/spring-beans/6.2.19/spring-beans-6.2.19.jar:/Users/maddy/.m2/repository/org/springframework/spring-core/6.2.19/spring-core-6.2.19.jar:/Users/maddy/.m2/repository/org/springframework/spring-jcl/6.2.19/spring-jcl-6.2.19.jar:/Users/maddy/.m2/repository/org/springframework/spring-web/6.2.19/spring-web-6.2.19.jar:/Users/maddy/.m2/repository/io/micrometer/micrometer-observation/1.15.12/micrometer-observation-1.15.12.jar:/Users/maddy/.m2/repository/io/micrometer/micrometer-commons/1.15.12/micrometer-commons-1.15.12.jar:/Users/maddy/.m2/repository/io/projectreactor/reactor-core/3.7.19/reactor-core-3.7.19.jar:/Users/maddy/.m2/repository/org/reactivestreams/reactive-streams/1.0.4/reactive-streams-1.0.4.jar:/Users/maddy/.m2/repository/io/projectreactor/netty/reactor-netty-http/1.2.8/reactor-netty-http-1.2.8.jar:/Users/maddy/.m2/repository/io/netty/netty-codec-http/4.1.122.Final/netty-codec-http-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-common/4.1.122.Final/netty-common-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-buffer/4.1.122.Final/netty-buffer-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-transport/4.1.122.Final/netty-transport-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-codec/4.1.122.Final/netty-codec-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-handler/4.1.122.Final/netty-handler-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-codec-http2/4.1.122.Final/netty-codec-http2-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-resolver-dns/4.1.122.Final/netty-resolver-dns-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-resolver/4.1.122.Final/netty-resolver-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-codec-dns/4.1.122.Final/netty-codec-dns-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-resolver-dns-native-macos/4.1.122.Final/netty-resolver-dns-native-macos-4.1.122.Final-osx-x86_64.jar:/Users/maddy/.m2/repository/io/netty/netty-resolver-dns-classes-macos/4.1.122.Final/netty-resolver-dns-classes-macos-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-transport-native-epoll/4.1.122.Final/netty-transport-native-epoll-4.1.122.Final-linux-x86_64.jar:/Users/maddy/.m2/repository/io/netty/netty-transport-native-unix-common/4.1.122.Final/netty-transport-native-unix-common-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-transport-classes-epoll/4.1.122.Final/netty-transport-classes-epoll-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/projectreactor/netty/reactor-netty-core/1.2.8/reactor-netty-core-1.2.8.jar:/Users/maddy/.m2/repository/io/netty/netty-handler-proxy/4.1.122.Final/netty-handler-proxy-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-codec-socks/4.1.122.Final/netty-codec-socks-4.1.122.Final.jar:/Users/maddy/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.18.6/jackson-databind-2.18.6.jar:/Users/maddy/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.18.6/jackson-annotations-2.18.6.jar:/Users/maddy/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.18.6/jackson-core-2.18.6.jar:/Users/maddy/.m2/repository/com/nimbusds/nimbus-jose-jwt/10.4/nimbus-jose-jwt-10.4.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter/5.12.2/junit-jupiter-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.12.2/junit-jupiter-api-5.12.2.jar:/Users/maddy/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar:/Users/maddy/.m2/repository/org/junit/platform/junit-platform-commons/1.12.2/junit-platform-commons-1.12.2.jar:/Users/maddy/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.12.2/junit-jupiter-params-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-engine/5.12.2/junit-jupiter-engine-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/platform/junit-platform-engine/1.12.2/junit-platform-engine-1.12.2.jar:"/>
|
||||
<property name="java.vm.vendor" value="Microsoft"/>
|
||||
<property name="sun.arch.data.model" value="64"/>
|
||||
<property name="java.vendor.url" value="https://www.microsoft.com"/>
|
||||
<property name="os.name" value="Mac OS X"/>
|
||||
<property name="java.vm.specification.version" value="21"/>
|
||||
<property name="sun.java.launcher" value="SUN_STANDARD"/>
|
||||
<property name="user.country" value="US"/>
|
||||
<property name="sun.boot.library.path" value="/Users/maddy/Library/Java/JavaVirtualMachines/ms-21.0.8/Contents/Home/lib"/>
|
||||
<property name="sun.java.command" value="/Users/maddy/Projects/matrix/cygnus-cloud-client/target/surefire/surefirebooter-20260724215520397_3.jar /Users/maddy/Projects/matrix/cygnus-cloud-client/target/surefire 2026-07-24T21-55-20_356-jvmRun1 surefire-20260724215520397_1tmp surefire_0-20260724215520397_2tmp"/>
|
||||
<property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
|
||||
<property name="jdk.debug" value="release"/>
|
||||
<property name="test" value="CloudClientPropertiesTest"/>
|
||||
<property name="surefire.test.class.path" value="/Users/maddy/Projects/matrix/cygnus-cloud-client/target/test-classes:/Users/maddy/Projects/matrix/cygnus-cloud-client/target/classes:/Users/maddy/.m2/repository/org/springframework/spring-webflux/6.2.19/spring-webflux-6.2.19.jar:/Users/maddy/.m2/repository/org/springframework/spring-beans/6.2.19/spring-beans-6.2.19.jar:/Users/maddy/.m2/repository/org/springframework/spring-core/6.2.19/spring-core-6.2.19.jar:/Users/maddy/.m2/repository/org/springframework/spring-jcl/6.2.19/spring-jcl-6.2.19.jar:/Users/maddy/.m2/repository/org/springframework/spring-web/6.2.19/spring-web-6.2.19.jar:/Users/maddy/.m2/repository/io/micrometer/micrometer-observation/1.15.12/micrometer-observation-1.15.12.jar:/Users/maddy/.m2/repository/io/micrometer/micrometer-commons/1.15.12/micrometer-commons-1.15.12.jar:/Users/maddy/.m2/repository/io/projectreactor/reactor-core/3.7.19/reactor-core-3.7.19.jar:/Users/maddy/.m2/repository/org/reactivestreams/reactive-streams/1.0.4/reactive-streams-1.0.4.jar:/Users/maddy/.m2/repository/io/projectreactor/netty/reactor-netty-http/1.2.8/reactor-netty-http-1.2.8.jar:/Users/maddy/.m2/repository/io/netty/netty-codec-http/4.1.122.Final/netty-codec-http-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-common/4.1.122.Final/netty-common-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-buffer/4.1.122.Final/netty-buffer-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-transport/4.1.122.Final/netty-transport-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-codec/4.1.122.Final/netty-codec-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-handler/4.1.122.Final/netty-handler-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-codec-http2/4.1.122.Final/netty-codec-http2-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-resolver-dns/4.1.122.Final/netty-resolver-dns-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-resolver/4.1.122.Final/netty-resolver-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-codec-dns/4.1.122.Final/netty-codec-dns-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-resolver-dns-native-macos/4.1.122.Final/netty-resolver-dns-native-macos-4.1.122.Final-osx-x86_64.jar:/Users/maddy/.m2/repository/io/netty/netty-resolver-dns-classes-macos/4.1.122.Final/netty-resolver-dns-classes-macos-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-transport-native-epoll/4.1.122.Final/netty-transport-native-epoll-4.1.122.Final-linux-x86_64.jar:/Users/maddy/.m2/repository/io/netty/netty-transport-native-unix-common/4.1.122.Final/netty-transport-native-unix-common-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-transport-classes-epoll/4.1.122.Final/netty-transport-classes-epoll-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/projectreactor/netty/reactor-netty-core/1.2.8/reactor-netty-core-1.2.8.jar:/Users/maddy/.m2/repository/io/netty/netty-handler-proxy/4.1.122.Final/netty-handler-proxy-4.1.122.Final.jar:/Users/maddy/.m2/repository/io/netty/netty-codec-socks/4.1.122.Final/netty-codec-socks-4.1.122.Final.jar:/Users/maddy/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.18.6/jackson-databind-2.18.6.jar:/Users/maddy/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.18.6/jackson-annotations-2.18.6.jar:/Users/maddy/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.18.6/jackson-core-2.18.6.jar:/Users/maddy/.m2/repository/com/nimbusds/nimbus-jose-jwt/10.4/nimbus-jose-jwt-10.4.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter/5.12.2/junit-jupiter-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.12.2/junit-jupiter-api-5.12.2.jar:/Users/maddy/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar:/Users/maddy/.m2/repository/org/junit/platform/junit-platform-commons/1.12.2/junit-platform-commons-1.12.2.jar:/Users/maddy/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.12.2/junit-jupiter-params-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-engine/5.12.2/junit-jupiter-engine-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/platform/junit-platform-engine/1.12.2/junit-platform-engine-1.12.2.jar:"/>
|
||||
<property name="sun.cpu.endian" value="little"/>
|
||||
<property name="user.home" value="/Users/maddy"/>
|
||||
<property name="user.language" value="en"/>
|
||||
<property name="java.specification.vendor" value="Oracle Corporation"/>
|
||||
<property name="java.version.date" value="2025-07-15"/>
|
||||
<property name="java.home" value="/Users/maddy/Library/Java/JavaVirtualMachines/ms-21.0.8/Contents/Home"/>
|
||||
<property name="file.separator" value="/"/>
|
||||
<property name="basedir" value="/Users/maddy/Projects/matrix/cygnus-cloud-client"/>
|
||||
<property name="java.vm.compressedOopsMode" value="Zero based"/>
|
||||
<property name="line.separator" value=" "/>
|
||||
<property name="java.vm.specification.vendor" value="Oracle Corporation"/>
|
||||
<property name="java.specification.name" value="Java Platform API Specification"/>
|
||||
<property name="apple.awt.application.name" value="ForkedBooter"/>
|
||||
<property name="surefire.real.class.path" value="/Users/maddy/Projects/matrix/cygnus-cloud-client/target/surefire/surefirebooter-20260724215520397_3.jar"/>
|
||||
<property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
|
||||
<property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
|
||||
<property name="java.runtime.version" value="21.0.8+9-LTS"/>
|
||||
<property name="user.name" value="maddy"/>
|
||||
<property name="stdout.encoding" value="UTF-8"/>
|
||||
<property name="path.separator" value=":"/>
|
||||
<property name="os.version" value="26.5.2"/>
|
||||
<property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
|
||||
<property name="file.encoding" value="UTF-8"/>
|
||||
<property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
|
||||
<property name="java.vendor.version" value="Microsoft-11933201"/>
|
||||
<property name="localRepository" value="/Users/maddy/.m2/repository"/>
|
||||
<property name="java.vendor.url.bug" value="https://github.com/microsoft/openjdk/issues"/>
|
||||
<property name="java.io.tmpdir" value="/var/folders/1l/36214rdn79755j30lcnmgsqh0000gn/T/"/>
|
||||
<property name="java.version" value="21.0.8"/>
|
||||
<property name="user.dir" value="/Users/maddy/Projects/matrix/cygnus-cloud-client"/>
|
||||
<property name="os.arch" value="aarch64"/>
|
||||
<property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
|
||||
<property name="native.encoding" value="UTF-8"/>
|
||||
<property name="java.library.path" value="/Users/maddy/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
|
||||
<property name="java.vm.info" value="mixed mode, sharing"/>
|
||||
<property name="stderr.encoding" value="UTF-8"/>
|
||||
<property name="java.vendor" value="Microsoft"/>
|
||||
<property name="java.vm.version" value="21.0.8+9-LTS"/>
|
||||
<property name="sun.io.unicode.encoding" value="UnicodeBig"/>
|
||||
<property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
|
||||
<property name="java.class.version" value="65.0"/>
|
||||
</properties>
|
||||
<testcase name="configurationAcceptsCompleteMachineIdentity" classname="com.cygnus.client.CloudClientPropertiesTest" time="0.007"/>
|
||||
<testcase name="configurationRequiresMachineCredentials" classname="com.cygnus.client.CloudClientPropertiesTest" time="0.001"/>
|
||||
</testsuite>
|
||||
@@ -1,4 +0,0 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.cygnus.client.CloudClientPropertiesTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.016 s -- in com.cygnus.client.CloudClientPropertiesTest
|
||||
Binary file not shown.
@@ -53,10 +53,10 @@ on-premises gateway. The client assertion must be:
|
||||
- bound to the configured client ID, installation ID, and token audience;
|
||||
- unexpired and no longer-lived than `CYGNUS_ASSERTION_TTL`.
|
||||
|
||||
The endpoint returns a short-lived RS256 access token carrying `client_id`,
|
||||
`installation_id`, and the approved scope. The identity endpoint requires the
|
||||
`identity.login` scope and verifies the same machine binding in the encrypted
|
||||
login payload.
|
||||
The endpoint returns a short-lived RS256 access token carrying the client,
|
||||
installation, tenant, license, security-version, and approved-scope claims.
|
||||
The identity endpoint requires the `identity.login` scope and verifies the
|
||||
same machine and tenant binding in the encrypted login payload.
|
||||
|
||||
Generate separate cloud key pairs:
|
||||
|
||||
@@ -73,26 +73,28 @@ openssl pkey -in config/keys/access-token-private.pem -pubout \
|
||||
chmod 600 config/keys/*private.pem
|
||||
```
|
||||
|
||||
Configure clients in an external Spring YAML file rather than the packaged
|
||||
`application.yml`:
|
||||
## Dynamic tenant, installation, and license registration
|
||||
|
||||
```yaml
|
||||
cygnus:
|
||||
security:
|
||||
enabled: true
|
||||
issuer-uri: https://cloud.example.com
|
||||
audience: cygnus-cloud-api
|
||||
token-audience: https://cloud.example.com/oauth2/token
|
||||
clients:
|
||||
customer-a:
|
||||
enabled: true
|
||||
installation-id: site-01
|
||||
assertion-public-key: file:/secure/clients/customer-a/public.pem
|
||||
scopes:
|
||||
- identity.login
|
||||
```
|
||||
Machine clients are no longer configured in a runtime `clients.yml`. The
|
||||
authoritative records are:
|
||||
|
||||
Start with that protected file using
|
||||
`--spring.config.additional-location=file:/secure/cygnus/clients.yml`.
|
||||
Never place cloud private keys, customer assertions, or installation private
|
||||
keys in the repository or container image.
|
||||
- `identity.client_account`: tenant identity and status;
|
||||
- `identity.client_installation`: machine identity, assertion public key,
|
||||
allowed scopes, enabled state, and security version;
|
||||
- `identity.client_license`: subscription period, package, type, status, and
|
||||
licensed limits.
|
||||
|
||||
Token issuance resolves the installation and active license through a
|
||||
Redis cache-aside service with PostgreSQL fallback. Cache entries have a
|
||||
bounded TTL and can be invalidated after administrative changes. Therefore,
|
||||
new customers, installations, key rotations, scope changes, and license
|
||||
changes do not require restarting the cloud service.
|
||||
|
||||
The login/menu queries are tenant-scoped. Tenant-owned identity tables carry
|
||||
`tenant_id`; `identity.pages` remains the shared feature catalog while
|
||||
permissions are assigned per tenant.
|
||||
|
||||
Use `scripts/setup-local-communication.sh` to create keys, register or update
|
||||
the database records, create the initial license, and generate the on-premises
|
||||
machine assertion. Never place cloud private keys, customer assertions, or
|
||||
installation private keys in the repository or container image.
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
|
||||
@@ -38,6 +38,15 @@ public class ReactiveCacheService {
|
||||
return redis.delete(cacheKey(namespace, key)).map(deleted -> deleted > 0);
|
||||
}
|
||||
|
||||
public Mono<Long> increment(String namespace, String key, Duration ttl) {
|
||||
String fullKey = cacheKey(namespace, key);
|
||||
return redis.opsForValue()
|
||||
.increment(fullKey)
|
||||
.flatMap(count -> count == 1
|
||||
? redis.expire(fullKey, ttl).thenReturn(count)
|
||||
: Mono.just(count));
|
||||
}
|
||||
|
||||
private String cacheKey(String namespace, String key) {
|
||||
return properties.keyPrefix() + ':' + namespace + ':' + key;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.cygnus.cloud.database;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import io.vertx.sqlclient.RowSet;
|
||||
import io.vertx.sqlclient.SqlConnection;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import java.util.function.Function;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -29,4 +31,37 @@ public class ReactiveDatabaseClient {
|
||||
return Mono.fromCompletionStage(
|
||||
() -> pool.preparedQuery(sql).execute(parameters).toCompletionStage());
|
||||
}
|
||||
|
||||
public Mono<Integer> preparedUpdate(String sql, Tuple parameters) {
|
||||
return preparedQuery(sql, parameters).map(RowSet::rowCount);
|
||||
}
|
||||
|
||||
public <T> Mono<T> inTransaction(Function<SqlConnection, Mono<T>> work) {
|
||||
return Mono.usingWhen(
|
||||
Mono.fromCompletionStage(() -> pool.getConnection().toCompletionStage()),
|
||||
connection -> Mono.fromCompletionStage(
|
||||
() -> connection.begin().toCompletionStage())
|
||||
.flatMap(transaction -> work.apply(connection)
|
||||
.flatMap(result -> Mono.fromCompletionStage(
|
||||
() -> transaction.commit().toCompletionStage())
|
||||
.thenReturn(result))
|
||||
.onErrorResume(error -> Mono.fromCompletionStage(
|
||||
() -> transaction.rollback().toCompletionStage())
|
||||
.onErrorResume(ignored -> Mono.empty())
|
||||
.then(Mono.error(error)))),
|
||||
connection -> Mono.fromCompletionStage(
|
||||
() -> connection.close().toCompletionStage()),
|
||||
(connection, error) -> Mono.fromCompletionStage(
|
||||
() -> connection.close().toCompletionStage()),
|
||||
connection -> Mono.fromCompletionStage(
|
||||
() -> connection.close().toCompletionStage()));
|
||||
}
|
||||
|
||||
public Mono<RowSet<Row>> preparedQuery(
|
||||
SqlConnection connection, String sql, Tuple parameters) {
|
||||
return Mono.fromCompletionStage(
|
||||
() -> connection.preparedQuery(sql)
|
||||
.execute(parameters)
|
||||
.toCompletionStage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.cygnus.cloud.identity.service.LoginRequestReplayService;
|
||||
import jakarta.validation.Valid;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.UUID;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
@@ -64,12 +65,21 @@ public class CloudLoginController {
|
||||
return Mono.error(new AuthenticationException("Login request replayed"));
|
||||
}
|
||||
return authenticationService.authenticate(
|
||||
tenantId(machineJwt),
|
||||
payload.loginId(),
|
||||
payload.password(),
|
||||
remoteAddress(serverRequest));
|
||||
});
|
||||
}
|
||||
|
||||
private UUID tenantId(Jwt jwt) {
|
||||
try {
|
||||
return UUID.fromString(jwt.getClaimAsString("tenant_id"));
|
||||
} catch (RuntimeException exception) {
|
||||
throw new AuthenticationException("Machine tenant is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePayload(LoginPayload payload) {
|
||||
if (payload == null
|
||||
|| !StringUtils.hasText(payload.loginId())
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.cygnus.cloud.identity.api;
|
||||
|
||||
import com.cygnus.cloud.identity.service.AuthenticationException;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
@@ -9,10 +11,12 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class IdentityErrorHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(IdentityErrorHandler.class);
|
||||
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
Map<String, String> authenticationFailure() {
|
||||
Map<String, String> authenticationFailure(AuthenticationException exception) {
|
||||
LOGGER.warn("Identity authentication rejected: {}", exception.getMessage());
|
||||
return Map.of("code", "AUTHENTICATION_FAILED", "message", "Authentication failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -20,25 +21,32 @@ public class IdentityRepository {
|
||||
g.name AS group_name, u.branch_id, b.branchname, b.branchcode, b.city,
|
||||
u.company_id, c.companyname, c.companycode, u.isactive
|
||||
FROM identity.app_user u
|
||||
JOIN identity.user_group g ON g.group_id = u.group_id
|
||||
JOIN identity.company c ON c.company_id = u.company_id
|
||||
JOIN identity.user_group g
|
||||
ON g.tenant_id = u.tenant_id AND g.group_id = u.group_id
|
||||
JOIN identity.company c
|
||||
ON c.tenant_id = u.tenant_id AND c.company_id = u.company_id
|
||||
JOIN identity.company_branch b
|
||||
ON b.branch_id = u.branch_id AND b.company_id = u.company_id
|
||||
WHERE upper(u.loginid) = upper($1)
|
||||
ON b.tenant_id = u.tenant_id
|
||||
AND b.branch_id = u.branch_id
|
||||
AND b.company_id = u.company_id
|
||||
WHERE u.tenant_id = $1
|
||||
AND upper(u.loginid) = upper($2)
|
||||
""";
|
||||
|
||||
private static final String FIND_MENU = """
|
||||
SELECT p.page_id, p.menulabel, p.targeturl, p.parentpage, p.pageorder,
|
||||
permissions.permission, p.targetwindow, permissions.requestval
|
||||
FROM identity.permission permissions
|
||||
FROM identity.permission permissions
|
||||
JOIN identity.pages p ON p.page_id = permissions.page_id
|
||||
WHERE permissions.group_id = $1
|
||||
WHERE permissions.tenant_id = $1
|
||||
AND permissions.group_id = $2
|
||||
AND p.isvisible = 1
|
||||
AND permissions.permission <> '000'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM identity.denied_pages denied
|
||||
WHERE denied.user_id = $2
|
||||
WHERE denied.tenant_id = permissions.tenant_id
|
||||
AND denied.user_id = $3
|
||||
AND denied.page_id = permissions.page_id
|
||||
AND denied.isdenied = 1
|
||||
)
|
||||
@@ -47,8 +55,8 @@ public class IdentityRepository {
|
||||
|
||||
private static final String RECORD_LOGIN = """
|
||||
INSERT INTO identity.user_loginhistory
|
||||
(loginid, logintime, ipaddr, user_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
(tenant_id, loginid, logintime, ipaddr, user_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING uid
|
||||
""";
|
||||
|
||||
@@ -60,23 +68,34 @@ public class IdentityRepository {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public Flux<IdentityUser> findUsersByLoginId(String loginId) {
|
||||
return database.preparedQuery(FIND_USER, Tuple.of(loginId))
|
||||
public Flux<IdentityUser> findUsersByLoginId(UUID tenantId, String loginId) {
|
||||
return database.preparedQuery(FIND_USER, Tuple.of(tenantId, loginId))
|
||||
.flatMapMany(rows -> Flux.fromIterable(rows).map(mapper::user));
|
||||
}
|
||||
|
||||
public Mono<List<MenuItem>> findMenu(short groupId, short userId) {
|
||||
return database.preparedQuery(FIND_MENU, Tuple.of(groupId, userId))
|
||||
public Mono<List<MenuItem>> findMenu(
|
||||
UUID tenantId, short groupId, short userId) {
|
||||
return database.preparedQuery(
|
||||
FIND_MENU, Tuple.of(tenantId, groupId, userId))
|
||||
.flatMapMany(rows -> Flux.fromIterable(rows).map(mapper::menuItem))
|
||||
.collectList();
|
||||
}
|
||||
|
||||
public Mono<Long> recordLogin(
|
||||
String loginId, Instant loginTime, String remoteAddress, short userId) {
|
||||
UUID tenantId,
|
||||
String loginId,
|
||||
Instant loginTime,
|
||||
String remoteAddress,
|
||||
short userId) {
|
||||
LocalDateTime databaseTime = LocalDateTime.ofInstant(loginTime, ZoneOffset.UTC);
|
||||
return database.preparedQuery(
|
||||
RECORD_LOGIN,
|
||||
Tuple.of(loginId, databaseTime, remoteAddress, userId))
|
||||
Tuple.of(
|
||||
tenantId,
|
||||
loginId,
|
||||
databaseTime,
|
||||
remoteAddress,
|
||||
userId))
|
||||
.map(rows -> rows.iterator().next().getLong("uid"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.cygnus.cloud.identity.model.IdentityUser;
|
||||
import com.cygnus.cloud.identity.repository.IdentityRepository;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -25,8 +26,11 @@ public class IdentityAuthenticationService {
|
||||
}
|
||||
|
||||
public Mono<AuthenticatedIdentity> authenticate(
|
||||
String loginId, String password, String remoteAddress) {
|
||||
return repository.findUsersByLoginId(loginId)
|
||||
UUID tenantId,
|
||||
String loginId,
|
||||
String password,
|
||||
String remoteAddress) {
|
||||
return repository.findUsersByLoginId(tenantId, loginId)
|
||||
.collectList()
|
||||
.flatMap(users -> {
|
||||
if (users.isEmpty()) {
|
||||
@@ -49,9 +53,14 @@ public class IdentityAuthenticationService {
|
||||
return Mono.error(new AuthenticationException("Invalid credentials"));
|
||||
}
|
||||
Instant loginTime = clock.instant();
|
||||
return repository.findMenu(user.groupId(), user.userId())
|
||||
return repository.findMenu(
|
||||
tenantId, user.groupId(), user.userId())
|
||||
.flatMap(menu -> repository.recordLogin(
|
||||
user.loginId(), loginTime, remoteAddress, user.userId())
|
||||
tenantId,
|
||||
user.loginId(),
|
||||
loginTime,
|
||||
remoteAddress,
|
||||
user.userId())
|
||||
.thenReturn(toAuthenticatedIdentity(user, loginTime, menu)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.cygnus.cloud.platform.api;
|
||||
|
||||
import com.cygnus.cloud.platform.service.ScopedDataException;
|
||||
import com.cygnus.cloud.platform.service.ScopedDataService;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/platform/data")
|
||||
public class ScopedDataController {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ScopedDataController.class);
|
||||
private final ScopedDataService service;
|
||||
|
||||
public ScopedDataController(ScopedDataService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Flux<ScopedDataItem> data(
|
||||
@AuthenticationPrincipal Jwt machineJwt,
|
||||
@Valid @RequestBody ScopedDataRequest request) {
|
||||
if (machineJwt == null) throw new ScopedDataException("Machine authentication required");
|
||||
try {
|
||||
return service.fetch(UUID.fromString(machineJwt.getClaimAsString("tenant_id")), request);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new ScopedDataException("Machine tenant is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(ScopedDataException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
Map<String, String> invalidRequest(ScopedDataException exception) {
|
||||
LOGGER.warn("Scoped platform-data request rejected: {}", exception.getMessage());
|
||||
return Map.of("code", "PLATFORM_DATA_INVALID", "message", exception.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cygnus.cloud.platform.api;
|
||||
|
||||
public record ScopedDataItem(
|
||||
Object value,
|
||||
String label,
|
||||
String group) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cygnus.cloud.platform.api;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
|
||||
public record ScopedDataRequest(
|
||||
@NotBlank String scope,
|
||||
@NotNull Map<String, Object> data) {
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.cygnus.cloud.platform.repository;
|
||||
|
||||
import com.cygnus.cloud.database.ReactiveDatabaseClient;
|
||||
import com.cygnus.cloud.platform.api.ScopedDataItem;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@Repository
|
||||
public class ScopedDataRepository {
|
||||
private static final String COMPANY_OPTIONS = """
|
||||
SELECT ov.opt_value AS value,
|
||||
ov.val_description AS label,
|
||||
lower(o.description) || '.' AS group_name
|
||||
FROM platform.company_options_mapping m
|
||||
JOIN platform.options o ON o.option_id = m.option_id
|
||||
JOIN platform.options_values ov ON ov.opt_value_id = m.opt_value_id
|
||||
JOIN identity.company c ON c.company_id = m.company_id
|
||||
WHERE c.tenant_id = $1
|
||||
AND m.company_id = $2
|
||||
AND o.isactive = 1
|
||||
AND ov.isactive = 1
|
||||
AND o.description = ANY($3::text[])
|
||||
ORDER BY o.description, ov.val_description
|
||||
""";
|
||||
private static final String PORTFOLIO_OPTIONS = """
|
||||
SELECT ov.opt_value AS value,
|
||||
ov.val_description AS label,
|
||||
lower(o.description) || '.' AS group_name
|
||||
FROM platform.options_mapping m
|
||||
JOIN platform.options o ON o.option_id = m.option_id
|
||||
JOIN platform.options_values ov ON ov.opt_value_id = m.opt_value_id
|
||||
JOIN identity.company c ON c.company_id = m.company_id
|
||||
JOIN platform.company_branch_portfolios cbp
|
||||
ON cbp.company_id = m.company_id
|
||||
AND cbp.portfolio_reference = m.portfolio_reference
|
||||
WHERE c.tenant_id = $1
|
||||
AND m.company_id = $2
|
||||
AND cbp.company_branch_id = $3
|
||||
AND m.portfolio_reference = $4
|
||||
AND o.isactive = 1
|
||||
AND ov.isactive = 1
|
||||
AND o.description = ANY($5::text[])
|
||||
ORDER BY o.description, ov.val_description
|
||||
""";
|
||||
|
||||
private final ReactiveDatabaseClient database;
|
||||
|
||||
public ScopedDataRepository(ReactiveDatabaseClient database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public Flux<ScopedDataItem> companyOptions(
|
||||
UUID tenantId, short companyId, List<String> descriptions) {
|
||||
return rows(COMPANY_OPTIONS, Tuple.of(tenantId, companyId, descriptions.toArray(String[]::new)));
|
||||
}
|
||||
|
||||
public Flux<ScopedDataItem> portfolioOptions(
|
||||
UUID tenantId, short companyId, short branchId, UUID portfolioReference,
|
||||
List<String> descriptions) {
|
||||
return rows(PORTFOLIO_OPTIONS,
|
||||
Tuple.of(tenantId, companyId, branchId, portfolioReference,
|
||||
descriptions.toArray(String[]::new)));
|
||||
}
|
||||
|
||||
private Flux<ScopedDataItem> rows(String sql, Tuple parameters) {
|
||||
return database.preparedQuery(sql, parameters)
|
||||
.flatMapMany(result -> Flux.fromIterable(result))
|
||||
.map(this::item);
|
||||
}
|
||||
|
||||
private ScopedDataItem item(Row row) {
|
||||
return new ScopedDataItem(
|
||||
row.getValue("value"), row.getString("label"), row.getString("group_name"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cygnus.cloud.platform.service;
|
||||
|
||||
public class ScopedDataException extends RuntimeException {
|
||||
public ScopedDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.cygnus.cloud.platform.service;
|
||||
|
||||
import com.cygnus.cloud.platform.api.ScopedDataItem;
|
||||
import com.cygnus.cloud.platform.api.ScopedDataRequest;
|
||||
import com.cygnus.cloud.platform.repository.ScopedDataRepository;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@Service
|
||||
public class ScopedDataService {
|
||||
public static final String COMPANY_OPTIONS = "company-options";
|
||||
public static final String PORTFOLIO_OPTIONS = "portfolio-options";
|
||||
|
||||
private final ScopedDataRepository repository;
|
||||
|
||||
public ScopedDataService(ScopedDataRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public Flux<ScopedDataItem> fetch(UUID tenantId, ScopedDataRequest request) {
|
||||
Map<String, Object> data = request.data();
|
||||
short companyId = shortValue(data, "companyId");
|
||||
List<String> descriptions = descriptions(data);
|
||||
return switch (request.scope().trim().toLowerCase(Locale.ROOT)) {
|
||||
case COMPANY_OPTIONS -> repository.companyOptions(tenantId, companyId, descriptions);
|
||||
case PORTFOLIO_OPTIONS -> repository.portfolioOptions(
|
||||
tenantId, companyId, shortValue(data, "branchId"),
|
||||
uuidValue(data, "portfolioReference"), descriptions);
|
||||
default -> Flux.error(new ScopedDataException("Unsupported data scope"));
|
||||
};
|
||||
}
|
||||
|
||||
private UUID uuidValue(Map<String, Object> data, String name) {
|
||||
try {
|
||||
return UUID.fromString(String.valueOf(data.get(name)));
|
||||
} catch (RuntimeException exception) {
|
||||
throw new ScopedDataException(name + " must be a valid UUID");
|
||||
}
|
||||
}
|
||||
|
||||
private short shortValue(Map<String, Object> data, String name) {
|
||||
Object value = data.get(name);
|
||||
try {
|
||||
short result = value instanceof Number number
|
||||
? number.shortValue() : Short.parseShort(String.valueOf(value));
|
||||
if (result <= 0) throw new NumberFormatException();
|
||||
return result;
|
||||
} catch (RuntimeException exception) {
|
||||
throw new ScopedDataException(name + " must be a positive number");
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> descriptions(Map<String, Object> data) {
|
||||
Object value = data.get("descriptions");
|
||||
if (!(value instanceof List<?> values)) {
|
||||
throw new ScopedDataException("descriptions must be a non-empty list");
|
||||
}
|
||||
List<String> result = values.stream()
|
||||
.map(String::valueOf)
|
||||
.map(String::trim)
|
||||
.filter(text -> !text.isEmpty())
|
||||
.map(text -> text.toUpperCase(Locale.ROOT))
|
||||
.distinct()
|
||||
.toList();
|
||||
if (result.isEmpty() || result.size() > 20) {
|
||||
throw new ScopedDataException("descriptions must contain between 1 and 20 values");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
public record CloudQuery(int queryId, String query) {
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/queries")
|
||||
public class CloudQueryController {
|
||||
|
||||
private final QueryCatalogRepository repository;
|
||||
|
||||
public CloudQueryController(QueryCatalogRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@GetMapping("/{queryId}")
|
||||
public Mono<CloudQuery> query(
|
||||
@PathVariable @Min(1) int queryId) {
|
||||
return repository.findEnabled(queryId)
|
||||
.switchIfEmpty(Mono.error(new QueryNotFoundException(queryId)));
|
||||
}
|
||||
|
||||
@GetMapping("/key/{queryKey}")
|
||||
public Mono<CloudQuery> queryByKey(@PathVariable String queryKey) {
|
||||
return repository.findEnabled(queryKey)
|
||||
.switchIfEmpty(Mono.error(new QueryNotFoundException(queryKey)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
import com.cygnus.cloud.database.ReactiveDatabaseClient;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public class QueryCatalogRepository {
|
||||
|
||||
private static final String INITIALIZE = """
|
||||
CREATE SCHEMA IF NOT EXISTS platform;
|
||||
CREATE SEQUENCE IF NOT EXISTS platform.application_query_id_seq;
|
||||
CREATE TABLE IF NOT EXISTS platform.application_query (
|
||||
query_id integer PRIMARY KEY DEFAULT nextval('platform.application_query_id_seq'),
|
||||
query_key varchar(100),
|
||||
query_text text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
CONSTRAINT ck_platform_application_query_id
|
||||
CHECK (query_id > 0),
|
||||
CONSTRAINT ck_platform_application_query_text
|
||||
CHECK (length(btrim(query_text)) > 0)
|
||||
);
|
||||
ALTER TABLE platform.application_query
|
||||
ADD COLUMN IF NOT EXISTS query_key varchar(100);
|
||||
ALTER TABLE platform.application_query ALTER COLUMN query_id
|
||||
SET DEFAULT nextval('platform.application_query_id_seq');
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_platform_application_query_key
|
||||
ON platform.application_query (query_key) WHERE query_key IS NOT NULL;
|
||||
SELECT setval('platform.application_query_id_seq',
|
||||
greatest(coalesce((SELECT max(query_id) FROM platform.application_query), 0) + 1, 1), false)
|
||||
""";
|
||||
private static final String FIND = """
|
||||
SELECT query_id, query_text
|
||||
FROM platform.application_query
|
||||
WHERE query_id = $1
|
||||
AND enabled = true
|
||||
""";
|
||||
private static final String FIND_BY_KEY = """
|
||||
SELECT query_id, query_text
|
||||
FROM platform.application_query
|
||||
WHERE query_key = $1
|
||||
AND enabled = true
|
||||
""";
|
||||
private final ReactiveDatabaseClient database;
|
||||
|
||||
public QueryCatalogRepository(ReactiveDatabaseClient database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public Mono<Void> initialize() {
|
||||
return database.query(INITIALIZE).then();
|
||||
}
|
||||
|
||||
public Mono<CloudQuery> findEnabled(int queryId) {
|
||||
return database.preparedQuery(FIND, Tuple.of(queryId))
|
||||
.flatMapMany(rows -> reactor.core.publisher.Flux.fromIterable(rows))
|
||||
.next()
|
||||
.map(row -> new CloudQuery(
|
||||
row.getInteger("query_id"), row.getString("query_text")));
|
||||
}
|
||||
|
||||
public Mono<CloudQuery> findEnabled(String queryKey) {
|
||||
return database.preparedQuery(FIND_BY_KEY, Tuple.of(queryKey))
|
||||
.flatMapMany(rows -> reactor.core.publisher.Flux.fromIterable(rows))
|
||||
.next()
|
||||
.map(row -> new CloudQuery(row.getInteger("query_id"), row.getString("query_text")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class QueryCatalogSchemaInitializer implements ApplicationRunner {
|
||||
|
||||
private final QueryCatalogRepository repository;
|
||||
|
||||
public QueryCatalogSchemaInitializer(QueryCatalogRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments arguments) {
|
||||
repository.initialize()
|
||||
.block(Duration.ofMinutes(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public class QueryNotFoundException extends RuntimeException {
|
||||
|
||||
public QueryNotFoundException(int queryId) {
|
||||
super("Query was not found: " + queryId);
|
||||
}
|
||||
|
||||
public QueryNotFoundException(String queryKey) {
|
||||
super("Query was not found: " + queryKey);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,13 @@ class AccessTokenIssuer {
|
||||
.jwtID(UUID.randomUUID().toString())
|
||||
.claim("client_id", principal.clientId())
|
||||
.claim("installation_id", principal.installationId())
|
||||
.claim("installation_uuid",
|
||||
principal.internalInstallationId().toString())
|
||||
.claim("tenant_id", principal.tenantId().toString())
|
||||
.claim("license_id", principal.licenseId().toString())
|
||||
.claim("license_type", principal.licenseType())
|
||||
.claim("package_code", principal.packageCode())
|
||||
.claim("security_version", principal.securityVersion())
|
||||
.claim("scope", String.join(" ", scopes))
|
||||
.build();
|
||||
SignedJWT jwt = new SignedJWT(
|
||||
|
||||
@@ -7,29 +7,33 @@ import com.nimbusds.jose.crypto.RSADecrypter;
|
||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import com.cygnus.cloud.tenant.model.ClientInstallation;
|
||||
import com.cygnus.cloud.tenant.model.ClientLicense;
|
||||
import com.cygnus.cloud.tenant.service.TenantRegistrationService;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Component
|
||||
class ClientAssertionValidator {
|
||||
|
||||
private final CommunicationSecurityProperties properties;
|
||||
private final TenantRegistrationService registrations;
|
||||
private final Clock clock;
|
||||
|
||||
ClientAssertionValidator(CommunicationSecurityProperties properties, Clock clock) {
|
||||
ClientAssertionValidator(
|
||||
CommunicationSecurityProperties properties,
|
||||
TenantRegistrationService registrations,
|
||||
Clock clock) {
|
||||
this.properties = properties;
|
||||
this.registrations = registrations;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
MachineClientPrincipal validate(String clientId, String encryptedAssertion) {
|
||||
CommunicationSecurityProperties.MachineClient client =
|
||||
properties.clients() == null ? null : properties.clients().get(clientId);
|
||||
if (client == null || !client.enabled()) {
|
||||
throw invalid();
|
||||
}
|
||||
Mono<MachineClientPrincipal> validate(String clientId, String encryptedAssertion) {
|
||||
try {
|
||||
JWEObject jwe = JWEObject.parse(encryptedAssertion);
|
||||
if (!JWEAlgorithm.RSA_OAEP_256.equals(jwe.getHeader().getAlgorithm())
|
||||
@@ -40,25 +44,69 @@ class ClientAssertionValidator {
|
||||
PemKeyLoader.privateKey(properties.assertionDecryptionPrivateKey())));
|
||||
|
||||
SignedJWT signedJwt = SignedJWT.parse(jwe.getPayload().toString());
|
||||
if (!signedJwt.verify(new RSASSAVerifier(
|
||||
PemKeyLoader.publicKey(client.assertionPublicKey())))) {
|
||||
JWTClaimsSet claims = signedJwt.getJWTClaimsSet();
|
||||
String installationCode = claims.getStringClaim("installation_id");
|
||||
if (installationCode == null || installationCode.isBlank()) {
|
||||
throw invalid();
|
||||
}
|
||||
return registrations.findInstallation(clientId, installationCode)
|
||||
.switchIfEmpty(Mono.error(invalid()))
|
||||
.flatMap(installation -> registrations
|
||||
.findCurrentLicense(installation.tenantId(), clock.instant())
|
||||
.filter(license -> license.isActiveAt(clock.instant()))
|
||||
.switchIfEmpty(Mono.error(new MachineAuthenticationException(
|
||||
"Client license is not active")))
|
||||
.map(license -> verify(
|
||||
clientId, signedJwt, claims, installation, license))
|
||||
.flatMap(principal -> registrations
|
||||
.touchInstallation(
|
||||
installation.installationId(), clock.instant())
|
||||
.thenReturn(principal)))
|
||||
.onErrorMap(
|
||||
exception -> !(exception instanceof MachineAuthenticationException),
|
||||
exception -> new MachineAuthenticationException(
|
||||
"Invalid client assertion", exception));
|
||||
} catch (MachineAuthenticationException exception) {
|
||||
return Mono.error(exception);
|
||||
} catch (Exception exception) {
|
||||
return Mono.error(new MachineAuthenticationException(
|
||||
"Invalid client assertion", exception));
|
||||
}
|
||||
}
|
||||
|
||||
JWTClaimsSet claims = signedJwt.getJWTClaimsSet();
|
||||
validateClaims(clientId, client, claims);
|
||||
private MachineClientPrincipal verify(
|
||||
String clientId,
|
||||
SignedJWT signedJwt,
|
||||
JWTClaimsSet claims,
|
||||
ClientInstallation installation,
|
||||
ClientLicense license) {
|
||||
try {
|
||||
if (!signedJwt.verify(new RSASSAVerifier(
|
||||
PemKeyLoader.publicKey(installation.assertionPublicKey())))) {
|
||||
throw invalid();
|
||||
}
|
||||
validateClaims(clientId, installation, claims);
|
||||
return new MachineClientPrincipal(
|
||||
clientId, client.installationId(), client.scopes());
|
||||
clientId,
|
||||
installation.installationCode(),
|
||||
installation.tenantId(),
|
||||
installation.installationId(),
|
||||
license.licenseId(),
|
||||
license.licenseType(),
|
||||
license.packageCode(),
|
||||
installation.securityVersion(),
|
||||
installation.allowedScopes());
|
||||
} catch (MachineAuthenticationException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new MachineAuthenticationException("Invalid client assertion", exception);
|
||||
throw new MachineAuthenticationException(
|
||||
"Invalid client assertion", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateClaims(
|
||||
String clientId,
|
||||
CommunicationSecurityProperties.MachineClient client,
|
||||
ClientInstallation installation,
|
||||
JWTClaimsSet claims) throws Exception {
|
||||
Instant now = clock.instant();
|
||||
Date issuedAt = claims.getIssueTime();
|
||||
@@ -66,7 +114,7 @@ class ClientAssertionValidator {
|
||||
if (!clientId.equals(claims.getIssuer())
|
||||
|| !clientId.equals(claims.getSubject())
|
||||
|| !claims.getAudience().contains(properties.tokenAudience())
|
||||
|| !client.installationId().equals(
|
||||
|| !installation.installationCode().equals(
|
||||
claims.getStringClaim("installation_id"))
|
||||
|| issuedAt == null
|
||||
|| expiresAt == null
|
||||
|
||||
@@ -30,8 +30,18 @@ public class CloudSecurityConfiguration {
|
||||
.authorizeExchange(exchange -> exchange
|
||||
.pathMatchers("/actuator/health", "/actuator/info").permitAll()
|
||||
.pathMatchers("/oauth2/token").permitAll()
|
||||
.pathMatchers(
|
||||
"/api/v1/installations/activation/validate",
|
||||
"/api/v1/installations/register")
|
||||
.permitAll()
|
||||
.pathMatchers("/api/v1/identity/login")
|
||||
.hasAuthority("SCOPE_identity.login")
|
||||
.pathMatchers("/api/v1/queries/**")
|
||||
.hasAuthority("SCOPE_identity.login")
|
||||
.pathMatchers("/api/v1/platform/data")
|
||||
.hasAuthority("SCOPE_identity.login")
|
||||
.pathMatchers("/api/v1/admin/**")
|
||||
.hasAuthority("SCOPE_cygnus.admin")
|
||||
.anyExchange().authenticated())
|
||||
.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()))
|
||||
.build();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@@ -16,13 +14,5 @@ public record CommunicationSecurityProperties(
|
||||
Duration accessTokenTtl,
|
||||
String assertionDecryptionPrivateKey,
|
||||
String accessTokenPrivateKey,
|
||||
String accessTokenPublicKey,
|
||||
Map<String, MachineClient> clients) {
|
||||
|
||||
public record MachineClient(
|
||||
boolean enabled,
|
||||
String installationId,
|
||||
String assertionPublicKey,
|
||||
Set<String> scopes) {
|
||||
}
|
||||
String accessTokenPublicKey) {
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
record MachineClientPrincipal(
|
||||
String clientId,
|
||||
String installationId,
|
||||
UUID tenantId,
|
||||
UUID internalInstallationId,
|
||||
UUID licenseId,
|
||||
String licenseType,
|
||||
String packageCode,
|
||||
int securityVersion,
|
||||
Set<String> allowedScopes) {
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.cygnus.cloud.security;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -34,33 +33,6 @@ class MachineSecurityConfigurationValidator implements InitializingBean {
|
||||
requireText("access-token-private-key", properties.accessTokenPrivateKey());
|
||||
requireText("access-token-public-key", properties.accessTokenPublicKey());
|
||||
|
||||
Map<String, CommunicationSecurityProperties.MachineClient> clients =
|
||||
properties.clients();
|
||||
if (clients == null || clients.isEmpty()) {
|
||||
throw invalid("at least one machine client is required");
|
||||
}
|
||||
clients.forEach(this::validateClient);
|
||||
}
|
||||
|
||||
private void validateClient(
|
||||
String clientId,
|
||||
CommunicationSecurityProperties.MachineClient client) {
|
||||
requireText("clients.<client-id>", clientId);
|
||||
if (client == null) {
|
||||
throw invalid("client '" + clientId + "' has no configuration");
|
||||
}
|
||||
requireText(
|
||||
"clients." + clientId + ".installation-id",
|
||||
client.installationId());
|
||||
requireText(
|
||||
"clients." + clientId + ".assertion-public-key",
|
||||
client.assertionPublicKey());
|
||||
if (client.scopes() == null
|
||||
|| client.scopes().isEmpty()
|
||||
|| client.scopes().stream().anyMatch(this::isBlank)) {
|
||||
throw invalid(
|
||||
"clients." + clientId + ".scopes must contain valid scopes");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireUri(String name, String value) {
|
||||
|
||||
@@ -33,23 +33,27 @@ class MachineTokenController {
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
Mono<Map<String, Object>> token(ServerWebExchange exchange) {
|
||||
return exchange.getFormData().map(this::issueToken);
|
||||
return exchange.getFormData().flatMap(this::issueToken);
|
||||
}
|
||||
|
||||
Map<String, Object> issueToken(MultiValueMap<String, String> form) {
|
||||
Mono<Map<String, Object>> issueToken(MultiValueMap<String, String> form) {
|
||||
if (!CLIENT_CREDENTIALS.equals(form.getFirst("grant_type"))
|
||||
|| !ASSERTION_TYPE.equals(form.getFirst("client_assertion_type"))) {
|
||||
throw new MachineAuthenticationException("Unsupported token request");
|
||||
}
|
||||
String clientId = required(form, "client_id");
|
||||
MachineClientPrincipal principal = assertionValidator.validate(
|
||||
clientId, required(form, "client_assertion"));
|
||||
Set<String> requestedScopes = scopes(form.getFirst("scope"));
|
||||
if (requestedScopes.isEmpty()
|
||||
|| !principal.allowedScopes().containsAll(requestedScopes)) {
|
||||
throw new MachineAuthenticationException("Invalid requested scope");
|
||||
}
|
||||
return tokenIssuer.issue(principal, requestedScopes).asOAuthResponse();
|
||||
return assertionValidator.validate(
|
||||
clientId, required(form, "client_assertion"))
|
||||
.map(principal -> {
|
||||
if (requestedScopes.isEmpty()
|
||||
|| !principal.allowedScopes().containsAll(requestedScopes)) {
|
||||
throw new MachineAuthenticationException(
|
||||
"Invalid requested scope");
|
||||
}
|
||||
return tokenIssuer.issue(principal, requestedScopes)
|
||||
.asOAuthResponse();
|
||||
});
|
||||
}
|
||||
|
||||
private String required(MultiValueMap<String, String> form, String name) {
|
||||
|
||||
@@ -66,6 +66,9 @@ final class PemKeyLoader {
|
||||
if (location == null || location.isBlank()) {
|
||||
throw new IllegalArgumentException("RSA key location is not configured");
|
||||
}
|
||||
if (location.contains("-----BEGIN ")) {
|
||||
return location;
|
||||
}
|
||||
if (location.startsWith("classpath:")) {
|
||||
String resource = location.substring("classpath:".length());
|
||||
try (InputStream stream = Thread.currentThread()
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ActivationValidationRequest(
|
||||
@NotBlank @Size(max = 40) String clientCode,
|
||||
@NotBlank @Size(max = 80) String licenseKey,
|
||||
@NotNull UUID installationUuid,
|
||||
@NotBlank @Size(max = 40)
|
||||
@Pattern(regexp = "^[A-Za-z0-9._-]+$")
|
||||
String installerVersion) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ActivationValidationResponse(
|
||||
String activationToken,
|
||||
OffsetDateTime expiresAt,
|
||||
UUID tenantId,
|
||||
String tenantSlug,
|
||||
String packageCode,
|
||||
int maximumInstallations) {
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import com.cygnus.cloud.tenant.repository.ClientAdministrationRepository;
|
||||
import com.cygnus.cloud.tenant.service.LicenseKeyService;
|
||||
import com.cygnus.cloud.tenant.service.InstallationLifecycleService;
|
||||
import com.cygnus.cloud.tenant.service.RegistrationEmailService;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin")
|
||||
public class ClientAdministrationController {
|
||||
|
||||
private final ClientAdministrationRepository repository;
|
||||
private final LicenseKeyService licenseKeyService;
|
||||
private final RegistrationEmailService emailService;
|
||||
private final InstallationLifecycleService installationLifecycleService;
|
||||
|
||||
public ClientAdministrationController(
|
||||
ClientAdministrationRepository repository,
|
||||
LicenseKeyService licenseKeyService,
|
||||
RegistrationEmailService emailService,
|
||||
InstallationLifecycleService installationLifecycleService) {
|
||||
this.repository = repository;
|
||||
this.licenseKeyService = licenseKeyService;
|
||||
this.emailService = emailService;
|
||||
this.installationLifecycleService = installationLifecycleService;
|
||||
}
|
||||
|
||||
@PostMapping("/tenants/{tenantId}/installations/{installationId}/decommission")
|
||||
public Mono<?> decommission(
|
||||
@PathVariable("tenantId") UUID tenantId,
|
||||
@PathVariable("installationId") UUID installationId,
|
||||
@Valid @RequestBody RetireInstallationRequest request,
|
||||
Authentication authentication) {
|
||||
return installationLifecycleService.retire(
|
||||
tenantId,
|
||||
installationId,
|
||||
"DECOMMISSIONED",
|
||||
actor(authentication),
|
||||
request.reason());
|
||||
}
|
||||
|
||||
@PostMapping("/tenants/{tenantId}/installations/{installationId}/revoke")
|
||||
public Mono<?> revoke(
|
||||
@PathVariable("tenantId") UUID tenantId,
|
||||
@PathVariable("installationId") UUID installationId,
|
||||
@Valid @RequestBody RetireInstallationRequest request,
|
||||
Authentication authentication) {
|
||||
return installationLifecycleService.retire(
|
||||
tenantId,
|
||||
installationId,
|
||||
"REVOKED",
|
||||
actor(authentication),
|
||||
request.reason());
|
||||
}
|
||||
|
||||
@PostMapping("/client-registrations")
|
||||
public Mono<Map<String, UUID>> registration(
|
||||
@Valid @RequestBody CreateClientRegistrationRequest request,
|
||||
Authentication authentication) {
|
||||
UUID id = UUID.randomUUID();
|
||||
Tuple values = Tuple.tuple()
|
||||
.addUUID(id)
|
||||
.addString(request.clientCode())
|
||||
.addString(request.legalCompanyName())
|
||||
.addString(request.tradeName())
|
||||
.addString(request.pan())
|
||||
.addString(request.cin())
|
||||
.addString(request.gstNumber())
|
||||
.addString(request.billingAddressLine1())
|
||||
.addString(request.billingAddressLine2())
|
||||
.addString(request.billingCity())
|
||||
.addString(request.billingState())
|
||||
.addString(request.billingPostalCode())
|
||||
.addString(request.billingCountry())
|
||||
.addString(request.billingEmail())
|
||||
.addString(request.primaryContactName())
|
||||
.addString(request.primaryContactEmail())
|
||||
.addString(request.primaryContactNumber())
|
||||
.addString(request.alternateContactName())
|
||||
.addString(request.alternateContactEmail())
|
||||
.addString(request.alternateContactNumber())
|
||||
.addLocalDate(request.contractStartDate())
|
||||
.addLocalDate(request.contractEndDate())
|
||||
.addString("ACTIVE")
|
||||
.addString(actor(authentication));
|
||||
return repository.createRegistration(values)
|
||||
.flatMap(created -> created
|
||||
? Mono.just(Map.of("registrationId", id))
|
||||
: Mono.error(new IllegalStateException(
|
||||
"Registration was not created")));
|
||||
}
|
||||
|
||||
@PostMapping("/client-registrations/{registrationId}/tenants")
|
||||
public Mono<Map<String, UUID>> tenant(
|
||||
@PathVariable("registrationId") UUID registrationId,
|
||||
@Valid @RequestBody CreateTenantRequest request) {
|
||||
UUID id = UUID.randomUUID();
|
||||
return repository.createTenant(
|
||||
id, registrationId, request.clientSlug(), request.clientName())
|
||||
.flatMap(created -> created
|
||||
? Mono.just(Map.of("tenantId", id))
|
||||
: Mono.error(new IllegalStateException("Tenant was not created")));
|
||||
}
|
||||
|
||||
@PostMapping("/tenants/{tenantId}/licenses")
|
||||
public Mono<Map<String, UUID>> license(
|
||||
@PathVariable("tenantId") UUID tenantId,
|
||||
@Valid @RequestBody CreateLicenseRequest request) {
|
||||
if (!request.validUntil().isAfter(request.validFrom())) {
|
||||
return Mono.error(new IllegalArgumentException(
|
||||
"License end must be after start"));
|
||||
}
|
||||
UUID id = UUID.randomUUID();
|
||||
return repository.createLicense(
|
||||
id,
|
||||
tenantId,
|
||||
request.licenseType(),
|
||||
request.packageCode(),
|
||||
request.validFrom(),
|
||||
request.validUntil(),
|
||||
request.maximumUsers(),
|
||||
request.maximumInstallations())
|
||||
.flatMap(created -> created
|
||||
? Mono.just(Map.of("licenseId", id))
|
||||
: Mono.error(new IllegalStateException("License was not created")));
|
||||
}
|
||||
|
||||
@PostMapping("/tenants/{tenantId}/licenses/{licenseId}/activation-key")
|
||||
public Mono<Map<String, String>> activationKey(
|
||||
@PathVariable("tenantId") UUID tenantId,
|
||||
@PathVariable("licenseId") UUID licenseId,
|
||||
@Valid @RequestBody IssueLicenseKeyRequest request,
|
||||
Authentication authentication) {
|
||||
return licenseKeyService.issue(
|
||||
tenantId, licenseId, request.expiresAt(), actor(authentication))
|
||||
.flatMap(issued -> repository
|
||||
.findLicenseDeliveryDetails(tenantId, licenseId)
|
||||
.switchIfEmpty(Mono.error(
|
||||
new IllegalArgumentException("Tenant or license not found")))
|
||||
.flatMap(details -> emailService.sendLicense(
|
||||
details.primaryContactEmail(),
|
||||
details.clientCode(),
|
||||
details.tenantSlug(),
|
||||
details.packageCode(),
|
||||
details.maximumInstallations(),
|
||||
request.expiresAt(),
|
||||
issued)
|
||||
.onErrorResume(error -> licenseKeyService
|
||||
.revoke(issued.activationKeyId())
|
||||
.then(Mono.error(error)))
|
||||
.thenReturn(Map.of(
|
||||
"activationKeyId",
|
||||
issued.activationKeyId().toString(),
|
||||
"keyHint",
|
||||
issued.keyHint(),
|
||||
"delivery",
|
||||
"EMAIL_SENT"))));
|
||||
}
|
||||
|
||||
private String actor(Authentication authentication) {
|
||||
return authentication == null ? "system" : authentication.getName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record CreateClientRegistrationRequest(
|
||||
@NotBlank @Pattern(regexp = "^[A-Z0-9]+(?:-[A-Z0-9]+)*$")
|
||||
@Size(max = 40) String clientCode,
|
||||
@NotBlank @Size(max = 240) String legalCompanyName,
|
||||
@Size(max = 240) String tradeName,
|
||||
@Size(max = 20) String pan,
|
||||
@Size(max = 30) String cin,
|
||||
@Size(max = 30) String gstNumber,
|
||||
@Size(max = 300) String billingAddressLine1,
|
||||
@Size(max = 300) String billingAddressLine2,
|
||||
@Size(max = 120) String billingCity,
|
||||
@Size(max = 120) String billingState,
|
||||
@Size(max = 20) String billingPostalCode,
|
||||
@NotBlank @Pattern(regexp = "^[A-Z]{2}$") String billingCountry,
|
||||
@Email @Size(max = 254) String billingEmail,
|
||||
@Size(max = 160) String primaryContactName,
|
||||
@Email @Size(max = 254) String primaryContactEmail,
|
||||
@Size(max = 30) String primaryContactNumber,
|
||||
@Size(max = 160) String alternateContactName,
|
||||
@Email @Size(max = 254) String alternateContactEmail,
|
||||
@Size(max = 30) String alternateContactNumber,
|
||||
@NotNull LocalDate contractStartDate,
|
||||
LocalDate contractEndDate) {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public record CreateLicenseRequest(
|
||||
@NotBlank @Size(max = 30) String licenseType,
|
||||
@NotBlank @Size(max = 50) String packageCode,
|
||||
@NotNull OffsetDateTime validFrom,
|
||||
@NotNull OffsetDateTime validUntil,
|
||||
@Min(1) Integer maximumUsers,
|
||||
@Min(1) @Max(100) int maximumInstallations) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record CreateTenantRequest(
|
||||
@NotBlank @Pattern(regexp = "^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||
@Size(max = 80) String clientSlug,
|
||||
@NotBlank @Size(max = 200) String clientName) {
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import com.cygnus.cloud.tenant.model.RegisteredInstallation;
|
||||
import com.cygnus.cloud.tenant.service.InstallationActivationService;
|
||||
import com.cygnus.cloud.tenant.service.ActivationRateLimiter;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/installations")
|
||||
public class InstallationActivationController {
|
||||
|
||||
private final InstallationActivationService activationService;
|
||||
private final ActivationRateLimiter rateLimiter;
|
||||
|
||||
public InstallationActivationController(
|
||||
InstallationActivationService activationService,
|
||||
ActivationRateLimiter rateLimiter) {
|
||||
this.activationService = activationService;
|
||||
this.rateLimiter = rateLimiter;
|
||||
}
|
||||
|
||||
@PostMapping("/activation/validate")
|
||||
public Mono<ActivationValidationResponse> validate(
|
||||
@Valid @RequestBody ActivationValidationRequest request,
|
||||
ServerHttpRequest serverRequest) {
|
||||
String sourceIp = remoteAddress(serverRequest);
|
||||
return rateLimiter.check(sourceIp, request.clientCode())
|
||||
.then(activationService.validateAndCreateSession(
|
||||
request.clientCode(),
|
||||
request.licenseKey(),
|
||||
request.installationUuid(),
|
||||
sourceIp,
|
||||
request.installerVersion()))
|
||||
.map(session -> new ActivationValidationResponse(
|
||||
session.token(),
|
||||
session.expiresAt(),
|
||||
session.tenantId(),
|
||||
session.clientSlug(),
|
||||
session.packageCode(),
|
||||
session.maxInstallations()));
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public Mono<RegisteredInstallation> register(
|
||||
@Valid @RequestBody InstallationRegistrationRequest request) {
|
||||
return activationService.register(
|
||||
request.activationToken(),
|
||||
request.installationCode(),
|
||||
request.installationName(),
|
||||
request.assertionPublicKey(),
|
||||
request.softwareVersion(),
|
||||
request.environment());
|
||||
}
|
||||
|
||||
private String remoteAddress(ServerHttpRequest request) {
|
||||
return request.getRemoteAddress() == null
|
||||
? null
|
||||
: request.getRemoteAddress().getAddress().getHostAddress();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import com.cygnus.cloud.tenant.service.InstallationActivationException;
|
||||
import com.cygnus.cloud.tenant.service.InstallationCodeConflictException;
|
||||
import com.cygnus.cloud.tenant.service.LicenseKeyException;
|
||||
import com.cygnus.cloud.tenant.service.ActivationRateLimitException;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class InstallationActivationErrorHandler {
|
||||
|
||||
@ExceptionHandler(InstallationCodeConflictException.class)
|
||||
@ResponseStatus(HttpStatus.CONFLICT)
|
||||
Map<String, String> installationCodeConflict(
|
||||
InstallationCodeConflictException exception) {
|
||||
return Map.of(
|
||||
"code", "INSTALLATION_CODE_ALREADY_EXISTS",
|
||||
"message", exception.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler({LicenseKeyException.class, InstallationActivationException.class})
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
Map<String, String> activationFailure() {
|
||||
return Map.of(
|
||||
"code", "INSTALLATION_ACTIVATION_FAILED",
|
||||
"message", "Installation activation could not be completed");
|
||||
}
|
||||
|
||||
@ExceptionHandler(ActivationRateLimitException.class)
|
||||
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
|
||||
Map<String, String> rateLimited() {
|
||||
return Map.of(
|
||||
"code", "INSTALLATION_ACTIVATION_RATE_LIMITED",
|
||||
"message", "Too many activation attempts; retry later");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record InstallationRegistrationRequest(
|
||||
@NotBlank @Size(max = 100) String activationToken,
|
||||
@NotBlank @Size(max = 100)
|
||||
@Pattern(regexp = "^[A-Za-z0-9][A-Za-z0-9_-]*$")
|
||||
String installationCode,
|
||||
@NotBlank @Size(max = 160) String installationName,
|
||||
@NotBlank @Size(max = 8192) String assertionPublicKey,
|
||||
@NotBlank @Size(max = 40)
|
||||
@Pattern(regexp = "^[A-Za-z0-9._-]+$")
|
||||
String softwareVersion,
|
||||
@NotBlank @Size(max = 30)
|
||||
@Pattern(regexp = "^[A-Za-z0-9_-]+$")
|
||||
String environment) {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public record IssueLicenseKeyRequest(
|
||||
@NotNull OffsetDateTime expiresAt) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cygnus.cloud.tenant.api;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record RetireInstallationRequest(
|
||||
@NotBlank @Size(max = 500) String reason) {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
public enum ActivationKeyStatus {
|
||||
ACTIVE,
|
||||
LOCKED,
|
||||
REVOKED,
|
||||
EXPIRED
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ActivationSession(
|
||||
UUID activationSessionId,
|
||||
UUID registrationId,
|
||||
UUID tenantId,
|
||||
UUID licenseId,
|
||||
UUID installationUuid,
|
||||
String token,
|
||||
OffsetDateTime expiresAt,
|
||||
String clientCode,
|
||||
String clientSlug,
|
||||
String packageCode,
|
||||
int maxInstallations) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ClientAccount(
|
||||
UUID tenantId,
|
||||
String clientSlug,
|
||||
String clientName,
|
||||
ClientStatus status,
|
||||
OffsetDateTime createdAt,
|
||||
OffsetDateTime updatedAt) {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ClientInstallation(
|
||||
UUID installationId,
|
||||
UUID tenantId,
|
||||
String clientId,
|
||||
String installationCode,
|
||||
String assertionPublicKey,
|
||||
Set<String> allowedScopes,
|
||||
boolean enabled,
|
||||
int securityVersion,
|
||||
OffsetDateTime lastAuthenticatedAt) {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ClientLicense(
|
||||
UUID licenseId,
|
||||
UUID tenantId,
|
||||
String licenseType,
|
||||
String packageCode,
|
||||
Instant validFrom,
|
||||
Instant validUntil,
|
||||
LicenseStatus status,
|
||||
Integer maxUsers,
|
||||
Integer maxInstallations,
|
||||
OffsetDateTime updatedAt) {
|
||||
|
||||
public boolean isActiveAt(Instant instant) {
|
||||
return status == LicenseStatus.ACTIVE
|
||||
&& !instant.isBefore(validFrom)
|
||||
&& instant.isBefore(validUntil);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ClientRegistration(
|
||||
UUID registrationId,
|
||||
String clientCode,
|
||||
String legalCompanyName,
|
||||
String tradeName,
|
||||
String billingEmail,
|
||||
LocalDate contractStartDate,
|
||||
LocalDate contractEndDate,
|
||||
RegistrationStatus status,
|
||||
OffsetDateTime createdAt,
|
||||
OffsetDateTime updatedAt,
|
||||
int version) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
public enum ClientStatus {
|
||||
ACTIVE,
|
||||
SUSPENDED,
|
||||
CANCELLED
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record InstallationLifecycleResult(
|
||||
UUID installationId,
|
||||
UUID tenantId,
|
||||
String clientId,
|
||||
String installationCode,
|
||||
String status) {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record IssuedLicenseKey(
|
||||
UUID activationKeyId,
|
||||
String licenseKey,
|
||||
String keyHint) {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record LicenseActivationContext(
|
||||
LicenseActivationKey activationKey,
|
||||
String clientCode,
|
||||
RegistrationStatus registrationStatus,
|
||||
ClientStatus tenantStatus,
|
||||
String clientSlug,
|
||||
String clientName,
|
||||
String packageCode,
|
||||
String licenseType,
|
||||
LicenseStatus licenseStatus,
|
||||
Instant validFrom,
|
||||
Instant validUntil,
|
||||
int maxInstallations,
|
||||
int consumingInstallations) {
|
||||
|
||||
public boolean hasCapacity() {
|
||||
return consumingInstallations < maxInstallations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record LicenseActivationKey(
|
||||
UUID activationKeyId,
|
||||
UUID registrationId,
|
||||
UUID tenantId,
|
||||
UUID licenseId,
|
||||
String keyHash,
|
||||
String keyHint,
|
||||
ActivationKeyStatus status,
|
||||
OffsetDateTime expiresAt,
|
||||
int failedAttempts,
|
||||
int maximumAttempts,
|
||||
OffsetDateTime lockedUntil,
|
||||
OffsetDateTime createdAt,
|
||||
OffsetDateTime lastUsedAt) {
|
||||
|
||||
public boolean canAttemptAt(OffsetDateTime now) {
|
||||
if (expiresAt != null && !expiresAt.isAfter(now)) {
|
||||
return false;
|
||||
}
|
||||
if (status == ActivationKeyStatus.ACTIVE) {
|
||||
return failedAttempts < maximumAttempts;
|
||||
}
|
||||
return status == ActivationKeyStatus.LOCKED
|
||||
&& lockedUntil != null
|
||||
&& !lockedUntil.isAfter(now);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
public record LicenseDeliveryDetails(
|
||||
String clientCode,
|
||||
String tenantSlug,
|
||||
String packageCode,
|
||||
int maximumInstallations,
|
||||
String primaryContactEmail) {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
public enum LicenseStatus {
|
||||
ACTIVE,
|
||||
SUSPENDED,
|
||||
EXPIRED,
|
||||
CANCELLED
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record RegisteredInstallation(
|
||||
UUID installationId,
|
||||
UUID installationUuid,
|
||||
String clientId,
|
||||
String installationCode,
|
||||
int securityVersion,
|
||||
String status) {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cygnus.cloud.tenant.model;
|
||||
|
||||
public enum RegistrationStatus {
|
||||
DRAFT,
|
||||
ACTIVE,
|
||||
SUSPENDED,
|
||||
TERMINATED
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.cygnus.cloud.tenant.repository;
|
||||
|
||||
import com.cygnus.cloud.database.ReactiveDatabaseClient;
|
||||
import com.cygnus.cloud.tenant.model.LicenseDeliveryDetails;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public class ClientAdministrationRepository {
|
||||
|
||||
private static final String INSERT_REGISTRATION = """
|
||||
INSERT INTO identity.client_registration_details (
|
||||
registration_id, client_code, legal_company_name, trade_name,
|
||||
pan, cin, gst_number, billing_address_line1,
|
||||
billing_address_line2, billing_city, billing_state,
|
||||
billing_postal_code, billing_country, billing_email,
|
||||
primary_contact_name, primary_contact_email,
|
||||
primary_contact_number, alternate_contact_name,
|
||||
alternate_contact_email, alternate_contact_number,
|
||||
contract_start_date, contract_end_date, status, created_by)
|
||||
VALUES ($1, upper($2), $3, $4, $5, $6, $7, $8, $9, $10, $11,
|
||||
$12, upper($13), $14, $15, $16, $17, $18, $19, $20,
|
||||
$21, $22, $23, $24)
|
||||
""";
|
||||
|
||||
private static final String INSERT_TENANT = """
|
||||
INSERT INTO identity.client_account (
|
||||
tenant_id, registration_id, client_slug, client_name,
|
||||
status, security_version)
|
||||
VALUES ($1, $2, $3, $4, 'ACTIVE', 1)
|
||||
""";
|
||||
|
||||
private static final String INSERT_LICENSE = """
|
||||
INSERT INTO identity.client_license (
|
||||
license_id, tenant_id, license_type, package_code,
|
||||
valid_from, valid_until, status, max_users,
|
||||
max_installations)
|
||||
SELECT $1, account.tenant_id, $3, $4, $5, $6, 'ACTIVE', $7, $8
|
||||
FROM identity.client_account account
|
||||
WHERE account.tenant_id = $2
|
||||
AND account.status = 'ACTIVE'
|
||||
""";
|
||||
|
||||
private static final String FIND_LICENSE_DELIVERY_DETAILS = """
|
||||
SELECT registration.client_code,
|
||||
registration.primary_contact_email,
|
||||
account.client_slug,
|
||||
license.package_code,
|
||||
license.max_installations
|
||||
FROM identity.client_license license
|
||||
JOIN identity.client_account account
|
||||
ON account.tenant_id = license.tenant_id
|
||||
JOIN identity.client_registration_details registration
|
||||
ON registration.registration_id = account.registration_id
|
||||
WHERE license.tenant_id = $1
|
||||
AND license.license_id = $2
|
||||
""";
|
||||
|
||||
private final ReactiveDatabaseClient database;
|
||||
|
||||
public ClientAdministrationRepository(ReactiveDatabaseClient database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public Mono<Boolean> createRegistration(Tuple values) {
|
||||
return database.preparedUpdate(INSERT_REGISTRATION, values)
|
||||
.map(count -> count == 1);
|
||||
}
|
||||
|
||||
public Mono<Boolean> createTenant(
|
||||
UUID tenantId,
|
||||
UUID registrationId,
|
||||
String slug,
|
||||
String name) {
|
||||
return database.preparedUpdate(
|
||||
INSERT_TENANT,
|
||||
Tuple.of(tenantId, registrationId, slug, name))
|
||||
.map(count -> count == 1);
|
||||
}
|
||||
|
||||
public Mono<Boolean> createLicense(
|
||||
UUID licenseId,
|
||||
UUID tenantId,
|
||||
String licenseType,
|
||||
String packageCode,
|
||||
OffsetDateTime validFrom,
|
||||
OffsetDateTime validUntil,
|
||||
Integer maxUsers,
|
||||
int maxInstallations) {
|
||||
return database.preparedUpdate(
|
||||
INSERT_LICENSE,
|
||||
Tuple.of(
|
||||
licenseId,
|
||||
tenantId,
|
||||
licenseType,
|
||||
packageCode,
|
||||
validFrom,
|
||||
validUntil,
|
||||
maxUsers,
|
||||
maxInstallations))
|
||||
.map(count -> count == 1);
|
||||
}
|
||||
|
||||
public Mono<LicenseDeliveryDetails> findLicenseDeliveryDetails(
|
||||
UUID tenantId, UUID licenseId) {
|
||||
return database.preparedQuery(
|
||||
FIND_LICENSE_DELIVERY_DETAILS,
|
||||
Tuple.of(tenantId, licenseId))
|
||||
.flatMap(rows -> {
|
||||
var iterator = rows.iterator();
|
||||
if (!iterator.hasNext()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
var row = iterator.next();
|
||||
return Mono.just(new LicenseDeliveryDetails(
|
||||
row.getString("client_code"),
|
||||
row.getString("client_slug"),
|
||||
row.getString("package_code"),
|
||||
row.getInteger("max_installations"),
|
||||
row.getString("primary_contact_email")));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package com.cygnus.cloud.tenant.repository;
|
||||
|
||||
import com.cygnus.cloud.database.ReactiveDatabaseClient;
|
||||
import com.cygnus.cloud.tenant.model.ActivationSession;
|
||||
import com.cygnus.cloud.tenant.model.RegisteredInstallation;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import io.vertx.sqlclient.SqlConnection;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public class InstallationActivationRepository {
|
||||
|
||||
private static final String INSERT_SESSION = """
|
||||
INSERT INTO identity.installation_activation_session (
|
||||
activation_session_id, activation_key_id, registration_id,
|
||||
tenant_id, license_id, installation_uuid, token_hash,
|
||||
status, expires_at, source_ip, installer_version)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'PENDING', $8,
|
||||
CAST($9 AS text)::inet, $10)
|
||||
""";
|
||||
|
||||
private static final String LOCK_SESSION_AND_LICENSE = """
|
||||
SELECT session.activation_session_id, session.registration_id,
|
||||
session.tenant_id, session.license_id,
|
||||
session.installation_uuid, session.status AS session_status,
|
||||
session.expires_at,
|
||||
registration.client_code, registration.status AS registration_status,
|
||||
account.client_slug, account.status AS tenant_status,
|
||||
license.package_code, license.status AS license_status,
|
||||
license.valid_from, license.valid_until,
|
||||
license.max_installations,
|
||||
(
|
||||
SELECT count(*)::integer
|
||||
FROM identity.client_installation installation
|
||||
WHERE installation.tenant_id = session.tenant_id
|
||||
AND installation.license_id = session.license_id
|
||||
AND installation.status IN ('PENDING', 'ACTIVE', 'SUSPENDED')
|
||||
) AS consuming_installations
|
||||
FROM identity.installation_activation_session session
|
||||
JOIN identity.client_registration_details registration
|
||||
ON registration.registration_id = session.registration_id
|
||||
JOIN identity.client_account account
|
||||
ON account.registration_id = session.registration_id
|
||||
AND account.tenant_id = session.tenant_id
|
||||
JOIN identity.client_license license
|
||||
ON license.tenant_id = session.tenant_id
|
||||
AND license.license_id = session.license_id
|
||||
WHERE session.token_hash = $1
|
||||
FOR UPDATE OF session, license
|
||||
""";
|
||||
|
||||
private static final String INSERT_INSTALLATION = """
|
||||
INSERT INTO identity.client_installation (
|
||||
installation_id, tenant_id, client_id, installation_code,
|
||||
assertion_public_key, allowed_scopes, enabled,
|
||||
security_version, license_id, installation_uuid,
|
||||
installation_name, status, registered_at,
|
||||
software_version, environment)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, true, 1, $7, $8, $9,
|
||||
'ACTIVE', $10, $11, $12)
|
||||
""";
|
||||
|
||||
private static final String CONSUME_SESSION = """
|
||||
UPDATE identity.installation_activation_session
|
||||
SET status = 'CONSUMED', consumed_at = $2
|
||||
WHERE activation_session_id = $1
|
||||
AND status = 'PENDING'
|
||||
""";
|
||||
|
||||
private static final String INSERT_AUDIT = """
|
||||
INSERT INTO identity.installation_audit_event (
|
||||
audit_event_id, registration_id, tenant_id, license_id,
|
||||
installation_id, event_type, actor_type, actor_id,
|
||||
reason, event_data, occurred_at)
|
||||
VALUES ($1, $2, $3, $4, $5, 'INSTALLATION_REGISTERED',
|
||||
'INSTALLER', $6, 'License-authorized installation',
|
||||
$7::jsonb, $8)
|
||||
""";
|
||||
|
||||
private final ReactiveDatabaseClient database;
|
||||
|
||||
public InstallationActivationRepository(ReactiveDatabaseClient database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public Mono<Boolean> insertSession(
|
||||
UUID sessionId,
|
||||
UUID activationKeyId,
|
||||
UUID registrationId,
|
||||
UUID tenantId,
|
||||
UUID licenseId,
|
||||
UUID installationUuid,
|
||||
String tokenHash,
|
||||
OffsetDateTime expiresAt,
|
||||
String sourceIp,
|
||||
String installerVersion) {
|
||||
Tuple values = Tuple.tuple()
|
||||
.addUUID(sessionId)
|
||||
.addUUID(activationKeyId)
|
||||
.addUUID(registrationId)
|
||||
.addUUID(tenantId)
|
||||
.addUUID(licenseId)
|
||||
.addUUID(installationUuid)
|
||||
.addString(tokenHash)
|
||||
.addOffsetDateTime(expiresAt)
|
||||
.addString(sourceIp)
|
||||
.addString(installerVersion);
|
||||
return database.preparedUpdate(INSERT_SESSION, values).map(count -> count == 1);
|
||||
}
|
||||
|
||||
public Mono<RegisteredInstallation> register(
|
||||
String tokenHash,
|
||||
String installationCode,
|
||||
String installationName,
|
||||
String assertionPublicKey,
|
||||
Set<String> scopes,
|
||||
String softwareVersion,
|
||||
String environment,
|
||||
OffsetDateTime now) {
|
||||
return database.inTransaction(connection -> lockContext(connection, tokenHash)
|
||||
.switchIfEmpty(Mono.error(new IllegalArgumentException(
|
||||
"Activation session is invalid")))
|
||||
.flatMap(context -> validate(context, now))
|
||||
.flatMap(context -> insertInstallation(
|
||||
connection,
|
||||
context,
|
||||
installationCode,
|
||||
installationName,
|
||||
assertionPublicKey,
|
||||
scopes,
|
||||
softwareVersion,
|
||||
environment,
|
||||
now)));
|
||||
}
|
||||
|
||||
private Mono<RegistrationContext> lockContext(
|
||||
SqlConnection connection, String tokenHash) {
|
||||
return database.preparedQuery(
|
||||
connection, LOCK_SESSION_AND_LICENSE, Tuple.of(tokenHash))
|
||||
.flatMap(rows -> {
|
||||
java.util.Iterator<Row> iterator = rows.iterator();
|
||||
return iterator.hasNext()
|
||||
? Mono.just(context(iterator.next()))
|
||||
: Mono.empty();
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<RegistrationContext> validate(
|
||||
RegistrationContext context, OffsetDateTime now) {
|
||||
boolean valid = "PENDING".equals(context.sessionStatus)
|
||||
&& context.expiresAt.isAfter(now)
|
||||
&& "ACTIVE".equals(context.registrationStatus)
|
||||
&& "ACTIVE".equals(context.tenantStatus)
|
||||
&& "ACTIVE".equals(context.licenseStatus)
|
||||
&& !now.isBefore(context.validFrom)
|
||||
&& now.isBefore(context.validUntil)
|
||||
&& context.consumingInstallations < context.maxInstallations;
|
||||
return valid
|
||||
? Mono.just(context)
|
||||
: Mono.error(new IllegalStateException(
|
||||
"Activation, license, or installation capacity is invalid"));
|
||||
}
|
||||
|
||||
private Mono<RegisteredInstallation> insertInstallation(
|
||||
SqlConnection connection,
|
||||
RegistrationContext context,
|
||||
String installationCode,
|
||||
String installationName,
|
||||
String assertionPublicKey,
|
||||
Set<String> scopes,
|
||||
String softwareVersion,
|
||||
String environment,
|
||||
OffsetDateTime now) {
|
||||
UUID installationId = UUID.randomUUID();
|
||||
Tuple insert = Tuple.tuple()
|
||||
.addUUID(installationId)
|
||||
.addUUID(context.tenantId)
|
||||
.addString(context.clientSlug)
|
||||
.addString(installationCode)
|
||||
.addString(assertionPublicKey)
|
||||
.addArrayOfString(scopes.toArray(String[]::new))
|
||||
.addUUID(context.licenseId)
|
||||
.addUUID(context.installationUuid)
|
||||
.addString(installationName)
|
||||
.addOffsetDateTime(now)
|
||||
.addString(softwareVersion)
|
||||
.addString(environment);
|
||||
return database.preparedQuery(connection, INSERT_INSTALLATION, insert)
|
||||
.flatMap(rows -> rows.rowCount() == 1
|
||||
? consumeAndAudit(connection, context, installationId, now)
|
||||
: Mono.error(new IllegalStateException(
|
||||
"Installation could not be registered")))
|
||||
.thenReturn(new RegisteredInstallation(
|
||||
installationId,
|
||||
context.installationUuid,
|
||||
context.clientSlug,
|
||||
installationCode,
|
||||
1,
|
||||
"ACTIVE"));
|
||||
}
|
||||
|
||||
private Mono<Void> consumeAndAudit(
|
||||
SqlConnection connection,
|
||||
RegistrationContext context,
|
||||
UUID installationId,
|
||||
OffsetDateTime now) {
|
||||
return database.preparedQuery(
|
||||
connection,
|
||||
CONSUME_SESSION,
|
||||
Tuple.of(context.sessionId, now))
|
||||
.flatMap(rows -> rows.rowCount() == 1
|
||||
? database.preparedQuery(
|
||||
connection,
|
||||
INSERT_AUDIT,
|
||||
Tuple.of(
|
||||
UUID.randomUUID(),
|
||||
context.registrationId,
|
||||
context.tenantId,
|
||||
context.licenseId,
|
||||
installationId,
|
||||
context.installationUuid.toString(),
|
||||
"{}",
|
||||
now))
|
||||
: Mono.error(new IllegalStateException(
|
||||
"Activation session was already consumed")))
|
||||
.then();
|
||||
}
|
||||
|
||||
private RegistrationContext context(Row row) {
|
||||
return new RegistrationContext(
|
||||
row.getUUID("activation_session_id"),
|
||||
row.getUUID("registration_id"),
|
||||
row.getUUID("tenant_id"),
|
||||
row.getUUID("license_id"),
|
||||
row.getUUID("installation_uuid"),
|
||||
row.getString("session_status"),
|
||||
row.getOffsetDateTime("expires_at"),
|
||||
row.getString("client_code"),
|
||||
row.getString("registration_status"),
|
||||
row.getString("client_slug"),
|
||||
row.getString("tenant_status"),
|
||||
row.getString("package_code"),
|
||||
row.getString("license_status"),
|
||||
row.getOffsetDateTime("valid_from"),
|
||||
row.getOffsetDateTime("valid_until"),
|
||||
row.getInteger("max_installations"),
|
||||
row.getInteger("consuming_installations"));
|
||||
}
|
||||
|
||||
private record RegistrationContext(
|
||||
UUID sessionId,
|
||||
UUID registrationId,
|
||||
UUID tenantId,
|
||||
UUID licenseId,
|
||||
UUID installationUuid,
|
||||
String sessionStatus,
|
||||
OffsetDateTime expiresAt,
|
||||
String clientCode,
|
||||
String registrationStatus,
|
||||
String clientSlug,
|
||||
String tenantStatus,
|
||||
String packageCode,
|
||||
String licenseStatus,
|
||||
OffsetDateTime validFrom,
|
||||
OffsetDateTime validUntil,
|
||||
int maxInstallations,
|
||||
int consumingInstallations) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.cygnus.cloud.tenant.repository;
|
||||
|
||||
import com.cygnus.cloud.database.ReactiveDatabaseClient;
|
||||
import com.cygnus.cloud.tenant.model.InstallationLifecycleResult;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public class InstallationLifecycleRepository {
|
||||
|
||||
private static final String RETIRE = """
|
||||
WITH retired AS (
|
||||
UPDATE identity.client_installation
|
||||
SET status = $3,
|
||||
enabled = false,
|
||||
security_version = security_version + 1,
|
||||
retired_at = now(),
|
||||
retired_by = $4,
|
||||
retirement_reason = $5
|
||||
WHERE installation_id = $1
|
||||
AND tenant_id = $2
|
||||
AND status IN ('PENDING', 'ACTIVE', 'SUSPENDED')
|
||||
RETURNING installation_id, tenant_id, client_id,
|
||||
installation_code, license_id, status
|
||||
), audited AS (
|
||||
INSERT INTO identity.installation_audit_event (
|
||||
audit_event_id, registration_id, tenant_id, license_id,
|
||||
installation_id, event_type, actor_type, actor_id,
|
||||
reason, event_data)
|
||||
SELECT gen_random_uuid(), account.registration_id, retired.tenant_id,
|
||||
retired.license_id, retired.installation_id, retired.status,
|
||||
'ADMIN', $4, $5, '{}'::jsonb
|
||||
FROM retired
|
||||
JOIN identity.client_account account
|
||||
ON account.tenant_id = retired.tenant_id
|
||||
)
|
||||
SELECT installation_id, tenant_id, client_id, installation_code, status
|
||||
FROM retired
|
||||
""";
|
||||
|
||||
private final ReactiveDatabaseClient database;
|
||||
|
||||
public InstallationLifecycleRepository(ReactiveDatabaseClient database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public Mono<InstallationLifecycleResult> retire(
|
||||
UUID tenantId,
|
||||
UUID installationId,
|
||||
String status,
|
||||
String actor,
|
||||
String reason) {
|
||||
return database.preparedQuery(
|
||||
RETIRE,
|
||||
Tuple.of(installationId, tenantId, status, actor, reason))
|
||||
.flatMap(rows -> {
|
||||
var iterator = rows.iterator();
|
||||
if (!iterator.hasNext()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
var row = iterator.next();
|
||||
return Mono.just(new InstallationLifecycleResult(
|
||||
row.getUUID("installation_id"),
|
||||
row.getUUID("tenant_id"),
|
||||
row.getString("client_id"),
|
||||
row.getString("installation_code"),
|
||||
row.getString("status")));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.cygnus.cloud.tenant.repository;
|
||||
|
||||
import com.cygnus.cloud.database.ReactiveDatabaseClient;
|
||||
import com.cygnus.cloud.tenant.model.ActivationKeyStatus;
|
||||
import com.cygnus.cloud.tenant.model.ClientStatus;
|
||||
import com.cygnus.cloud.tenant.model.LicenseActivationContext;
|
||||
import com.cygnus.cloud.tenant.model.LicenseActivationKey;
|
||||
import com.cygnus.cloud.tenant.model.LicenseStatus;
|
||||
import com.cygnus.cloud.tenant.model.RegistrationStatus;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public class LicenseActivationRepository {
|
||||
|
||||
private static final String FIND_CANDIDATES = """
|
||||
SELECT activation.activation_key_id, activation.registration_id,
|
||||
activation.tenant_id, activation.license_id,
|
||||
activation.key_hash, activation.key_hint, activation.status,
|
||||
activation.expires_at, activation.failed_attempts,
|
||||
activation.maximum_attempts, activation.locked_until,
|
||||
activation.created_at, activation.last_used_at,
|
||||
registration.client_code,
|
||||
registration.status AS registration_status,
|
||||
account.status AS tenant_status,
|
||||
account.client_slug, account.client_name,
|
||||
license.package_code, license.license_type,
|
||||
license.status AS license_status,
|
||||
license.valid_from, license.valid_until,
|
||||
license.max_installations,
|
||||
(
|
||||
SELECT count(*)::integer
|
||||
FROM identity.client_installation installation
|
||||
WHERE installation.tenant_id = activation.tenant_id
|
||||
AND installation.license_id = activation.license_id
|
||||
AND installation.status IN ('PENDING', 'ACTIVE', 'SUSPENDED')
|
||||
) AS consuming_installations
|
||||
FROM identity.license_activation_key activation
|
||||
JOIN identity.client_registration_details registration
|
||||
ON registration.registration_id = activation.registration_id
|
||||
JOIN identity.client_account account
|
||||
ON account.registration_id = activation.registration_id
|
||||
AND account.tenant_id = activation.tenant_id
|
||||
JOIN identity.client_license license
|
||||
ON license.tenant_id = activation.tenant_id
|
||||
AND license.license_id = activation.license_id
|
||||
WHERE upper(registration.client_code) = upper($1)
|
||||
AND activation.key_hint = $2
|
||||
""";
|
||||
|
||||
private static final String INSERT_KEY = """
|
||||
INSERT INTO identity.license_activation_key (
|
||||
activation_key_id, registration_id, tenant_id, license_id,
|
||||
key_hash, key_hint, status, expires_at, created_by)
|
||||
SELECT $1, account.registration_id, account.tenant_id,
|
||||
license.license_id, $4, $5, 'ACTIVE', $6, $7
|
||||
FROM identity.client_account account
|
||||
JOIN identity.client_license license
|
||||
ON license.tenant_id = account.tenant_id
|
||||
WHERE account.tenant_id = $2
|
||||
AND license.license_id = $3
|
||||
AND account.status = 'ACTIVE'
|
||||
AND license.status = 'ACTIVE'
|
||||
""";
|
||||
|
||||
private static final String RECORD_SUCCESS = """
|
||||
UPDATE identity.license_activation_key
|
||||
SET failed_attempts = 0,
|
||||
locked_until = NULL,
|
||||
status = 'ACTIVE',
|
||||
last_used_at = $2
|
||||
WHERE activation_key_id = $1
|
||||
""";
|
||||
|
||||
private static final String RECORD_FAILURE = """
|
||||
UPDATE identity.license_activation_key
|
||||
SET failed_attempts = LEAST(failed_attempts + 1, maximum_attempts),
|
||||
status = CASE
|
||||
WHEN failed_attempts + 1 >= maximum_attempts THEN 'LOCKED'
|
||||
ELSE status
|
||||
END,
|
||||
locked_until = CASE
|
||||
WHEN failed_attempts + 1 >= maximum_attempts THEN $2
|
||||
ELSE locked_until
|
||||
END
|
||||
WHERE activation_key_id = $1
|
||||
""";
|
||||
|
||||
private static final String REVOKE_KEY = """
|
||||
UPDATE identity.license_activation_key
|
||||
SET status = 'REVOKED',
|
||||
locked_until = NULL,
|
||||
revoked_at = now(),
|
||||
revoked_by = 'system',
|
||||
revocation_reason = 'Activation-key delivery failed'
|
||||
WHERE activation_key_id = $1
|
||||
AND status IN ('ACTIVE', 'LOCKED')
|
||||
""";
|
||||
|
||||
private final ReactiveDatabaseClient database;
|
||||
|
||||
public LicenseActivationRepository(ReactiveDatabaseClient database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public Flux<LicenseActivationContext> findCandidates(
|
||||
String clientCode, String keyHint) {
|
||||
return database.preparedQuery(FIND_CANDIDATES, Tuple.of(clientCode, keyHint))
|
||||
.flatMapMany(Flux::fromIterable)
|
||||
.map(this::context);
|
||||
}
|
||||
|
||||
public Mono<Boolean> insert(
|
||||
UUID activationKeyId,
|
||||
UUID tenantId,
|
||||
UUID licenseId,
|
||||
String keyHash,
|
||||
String keyHint,
|
||||
OffsetDateTime expiresAt,
|
||||
String createdBy) {
|
||||
Tuple values = Tuple.tuple()
|
||||
.addUUID(activationKeyId)
|
||||
.addUUID(tenantId)
|
||||
.addUUID(licenseId)
|
||||
.addString(keyHash)
|
||||
.addString(keyHint)
|
||||
.addOffsetDateTime(expiresAt)
|
||||
.addString(createdBy);
|
||||
return database.preparedUpdate(INSERT_KEY, values).map(count -> count == 1);
|
||||
}
|
||||
|
||||
public Mono<Boolean> recordSuccess(UUID activationKeyId, OffsetDateTime now) {
|
||||
return database.preparedUpdate(
|
||||
RECORD_SUCCESS, Tuple.of(activationKeyId, now))
|
||||
.map(count -> count == 1);
|
||||
}
|
||||
|
||||
public Mono<Boolean> recordFailure(
|
||||
UUID activationKeyId, OffsetDateTime lockedUntil) {
|
||||
return database.preparedUpdate(
|
||||
RECORD_FAILURE, Tuple.of(activationKeyId, lockedUntil))
|
||||
.map(count -> count == 1);
|
||||
}
|
||||
|
||||
public Mono<Boolean> revoke(UUID activationKeyId) {
|
||||
return database.preparedUpdate(REVOKE_KEY, Tuple.of(activationKeyId))
|
||||
.map(count -> count == 1);
|
||||
}
|
||||
|
||||
private LicenseActivationContext context(Row row) {
|
||||
LicenseActivationKey key = new LicenseActivationKey(
|
||||
row.getUUID("activation_key_id"),
|
||||
row.getUUID("registration_id"),
|
||||
row.getUUID("tenant_id"),
|
||||
row.getUUID("license_id"),
|
||||
row.getString("key_hash"),
|
||||
row.getString("key_hint"),
|
||||
ActivationKeyStatus.valueOf(row.getString("status")),
|
||||
row.getOffsetDateTime("expires_at"),
|
||||
row.getInteger("failed_attempts"),
|
||||
row.getInteger("maximum_attempts"),
|
||||
row.getOffsetDateTime("locked_until"),
|
||||
row.getOffsetDateTime("created_at"),
|
||||
row.getOffsetDateTime("last_used_at"));
|
||||
return new LicenseActivationContext(
|
||||
key,
|
||||
row.getString("client_code"),
|
||||
RegistrationStatus.valueOf(row.getString("registration_status")),
|
||||
ClientStatus.valueOf(row.getString("tenant_status")),
|
||||
row.getString("client_slug"),
|
||||
row.getString("client_name"),
|
||||
row.getString("package_code"),
|
||||
row.getString("license_type"),
|
||||
LicenseStatus.valueOf(row.getString("license_status")),
|
||||
row.getOffsetDateTime("valid_from").toInstant(),
|
||||
row.getOffsetDateTime("valid_until").toInstant(),
|
||||
row.getInteger("max_installations"),
|
||||
row.getInteger("consuming_installations"));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user