Skip to content

Origo Tests #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 83 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
ac5fded
init integration project
ben-w-martin Jul 22, 2024
911417d
add: createCallbackSubscription method to OrigoClient
ben-w-martin Jul 23, 2024
97a2c50
committing to access on different device
ben-w-martin Jul 23, 2024
c5e7a92
add: base class for third party http responses, updated touchnet and …
ben-w-martin Jul 24, 2024
29d4f78
add: HttpActionResult wrapper allows for use of try/catch in sending …
ben-w-martin Jul 24, 2024
9aa52cb
refactor: checkexistingcallbacksubs and createcallbacksubscription ut…
ben-w-martin Jul 24, 2024
3f0e812
add: createFilter method
ben-w-martin Jul 24, 2024
c8d1036
refactor: naming conv. in httpactionresult, OrigoService will now aut…
ben-w-martin Jul 24, 2024
844c73a
add: uploadUserPhoto method. refactor: HttpActionResult result proper…
ben-w-martin Jul 24, 2024
1106df6
add: approvePhoto method
ben-w-martin Jul 24, 2024
e17860f
add: listEvents method to OrigoClient. Start developing origoSerivice.
ben-w-martin Jul 24, 2024
5adb36b
commit for switching working device
ben-w-martin Jul 24, 2024
2dd0a53
refactor: listEvents method accepts params for query string
ben-w-martin Jul 25, 2024
9aa8173
refactor: deciding where to place loggers, client or service. add: so…
ben-w-martin Jul 25, 2024
e2dceb2
add: OrigoEventLoggingService class configures local directory for st…
ben-w-martin Jul 25, 2024
8ba43a3
building theoretical stucture to origoservice in lieu of API key
ben-w-martin Jul 25, 2024
433f8be
add: writeEventToJson, event id:timestamp values will be stored local…
ben-w-martin Jul 25, 2024
5786eb9
add: json logs with timestamps / ids created for new test events
ben-w-martin Jul 26, 2024
9800e6d
add: scrubbed backup properties file
ben-w-martin Jul 26, 2024
f724f9a
add: OrigoEventLoggingService creates max of last 2 request logs, del…
ben-w-martin Jul 26, 2024
4e51661
refactor: event logs use timestamp of log generation, in the future w…
ben-w-martin Jul 26, 2024
d61e88f
refactor: Only one log is maintained at a time, idempotent to old eve…
ben-w-martin Jul 29, 2024
018bd2b
info: finishing for the day. Eventlogging now maintains one log file.…
ben-w-martin Jul 29, 2024
a753758
add: IOrigoStorageService interface. refactor: change implementing me…
ben-w-martin Jul 30, 2024
bd2199d
refactor: disuse HttpActionResult in favor of refactored origoRespons…
ben-w-martin Jul 31, 2024
0ec34fb
add: utils with some easy time manipulating methods. add: Origoservic…
ben-w-martin Jul 31, 2024
99f1466
refactor: api keys only go in app.properties
ben-w-martin Jul 31, 2024
e9b25ae
cleanup: removing unused classes. Refactor: reduce duplicate code in …
ben-w-martin Jul 31, 2024
f7866bc
add: cloudCardClient to host CC api methods. add: HttpClient to host …
ben-w-martin Jul 31, 2024
b325ca2
add: createfilter works properly
ben-w-martin Aug 1, 2024
35c8d3c
add: cloudcardservice, provisioningservice. successfully provisioning…
ben-w-martin Aug 1, 2024
e9806f5
add: PeopleService interface, OrigoPeopleService class.
ben-w-martin Aug 12, 2024
0973e4b
bug: needs simplification. There are too many layers, all too strongl…
ben-w-martin Aug 12, 2024
f06ebba
add: OrigoStorageService. focusing on getting photodownloader to work…
ben-w-martin Aug 12, 2024
2d91082
bug: origo rejecting photo for unsupported type.
ben-w-martin Aug 12, 2024
87dddec
fix: downloader stores photos to origo. Needs continued debugging and…
ben-w-martin Aug 13, 2024
8267adc
feature: OrigoStorageService successfully downloads photos to Mobile …
ben-w-martin Aug 13, 2024
eb3748c
refactor: cleanup code, rm comments
ben-w-martin Aug 13, 2024
b23381d
fix: unfinished comment
ben-w-martin Aug 13, 2024
13fd8d0
Merge branch 'master' into origo-storage
ben-w-martin Aug 14, 2024
edd645d
sanitized application.properties, rm'd from gitignore
ben-w-martin Aug 14, 2024
719b894
fix: update app.properties to master + new origo features
ben-w-martin Aug 14, 2024
c9ad64d
fix: update gitignore to match master
ben-w-martin Aug 14, 2024
07a589f
refactor: cleaning up redundant code
ben-w-martin Aug 14, 2024
ca572db
update: README shows config steps for Origo storage service
ben-w-martin Aug 14, 2024
68c8bb1
Update README.md
ben-w-martin Aug 14, 2024
0b890c5
rm unintended changes, unused files
ben-w-martin Aug 14, 2024
efb2f31
Delete src/main/groovy/com/cloudcard/photoDownloader/Utils.groovy
ben-w-martin Aug 14, 2024
9f89400
Update TouchNetClient.groovy
ben-w-martin Aug 14, 2024
1e7b946
Delete src/main/groovy/com/cloudcard/photoDownloader/PhotoStatusMessa…
ben-w-martin Aug 14, 2024
34f431b
Update TouchNetClient.groovy
ben-w-martin Aug 14, 2024
afdab19
Merge branch 'origo-storage' of github.com:ben-w-martin/cloudcard-pho…
ben-w-martin Aug 14, 2024
ffb71c6
Fixed: unintended changes to touchnetClient, build.gradle
ben-w-martin Aug 14, 2024
476570b
remove unused library
ben-w-martin Aug 15, 2024
0a8a11d
add: httpClientSpec, initial unit tests for origo storage.
ben-w-martin Aug 19, 2024
bcf8d3c
add: test to ensure failure with invalid URL
ben-w-martin Aug 19, 2024
5756ac7
add: tests for handleResponseLogging. Re-ran application with Origo A…
ben-w-martin Aug 19, 2024
9940e18
add OrigoClientSpec with assoc. refactors
ben-w-martin Aug 20, 2024
a6e845b
refactor OrigoClientSpec
ben-w-martin Aug 20, 2024
cb24745
BUG: after successful upload/approve, downloader service errors. Phot…
ben-w-martin Aug 21, 2024
9b66254
Fixed: was using findResult instead of findResults >:(
ben-w-martin Aug 21, 2024
4657b36
transfer devices
ben-w-martin Aug 21, 2024
8188859
add: more unit tests for origoStorageService. refactor the same.
ben-w-martin Aug 21, 2024
9559dc9
refactor origoStorageService.resolveFileType
ben-w-martin Aug 21, 2024
a7cd0ee
all tests pass. application origo storage service works as expected
ben-w-martin Aug 21, 2024
f3c66dd
fix: noticed potential bug with isAuthenticated property
ben-w-martin Aug 21, 2024
6a7db68
add given: block to each test with setup logic. Fixed possible infini…
ben-w-martin Aug 22, 2024
6fc0c03
add: initialization test to origoStorageServiceSpec
ben-w-martin Aug 22, 2024
1fcc500
some final refactoring. Tests are green. API requests work properly.
ben-w-martin Aug 22, 2024
28949fd
final tests, refactors, cleanup for PR.
ben-w-martin Aug 22, 2024
fdb8107
one more test, final, final cleanup
ben-w-martin Aug 22, 2024
25fffa2
rm comment
ben-w-martin Aug 22, 2024
bfbad57
fix: OrigoClient was missing conditionalOnProperty annotation
ben-w-martin Aug 23, 2024
3b8f3d3
feature: override current photo. Refactor: origoclient / origostorage…
ben-w-martin Aug 28, 2024
57c42f8
refactor origoClient, tests. All passing
ben-w-martin Aug 28, 2024
24136ef
24/25 tests passing for OrigoStorageServiceSpec
ben-w-martin Aug 28, 2024
022f255
replace HttpClient with SimpleResponseLogger, update tests
ben-w-martin Aug 29, 2024
5cf8cc7
Tests are green. Tested successfully with test-api's. Includes some r…
ben-w-martin Sep 2, 2024
c645508
Fix log in origoClient init method
ben-w-martin Sep 2, 2024
2ae79aa
fix Touchnet client spaces. Delete unused file
ben-w-martin Sep 2, 2024
a38dda9
add tests for responseWrapper
ben-w-martin Sep 2, 2024
8f49a1f
update application.properties to production api endpoints
ben-w-martin Sep 2, 2024
0be511a
fix bug in init method
ben-w-martin Sep 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ application-test.properties*
downloaded-photos
summary

lib/
lib/
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,45 @@ The downloader supports going through a proxy when using the CloudCardPhotoServi
- `TouchNetClient.originId`
- provided by CloudCard

#### Origo Storage Service Settings
*Note: `downloader.storageService` must be set to `OrigoStorageService` for these to have any effect.*
*Note: This integration does NOT YET support PKI authentication for Origo's production APIs.*
*Visit Origo's Mobile Access documentation for more information https://doc.origo.hidglobal.com/api/*

- `Origo.eventManagementApi`
- URI for accessing events in Mobile Access, such as user creation events.
- `Origo.mobileIdentitiesApi`
- URI for user and photo related requests.
- `Origo.certIdpApi`
- URI for Authentication requests.
- `Origo.organizationId`
- Provided by origo. First half of clientId.
- `Origo.accessToken`
- Temporary token obtained through authentication request. Only needs to be specified in certain development situations.
- `Origo.clientSecret`
- System account password or PKI token.
- Required for authentication.
- `Origo.clientId`
- Provided by Origo.
- Available in Origo Management Portal.
- `Origo.contentType`
- Used for request headers - see documentation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a situation where this isn't always the same value?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not unless there was a change in API Version. Would you like these properties to stay filled?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the moment, I've included the properties that don't change in the existing application.properties file.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a little mistake. While the content-type shouldn't change except for a couple requests (Already accounted for in OrigoClient methods), All of the API links change from test API to the Production API. I'll add the production API links to the Application.properties.

- `Origo.applicationVersion`
- Origo API version (2.2 as of Aug 2024).
- Used for request headers.
- `Origo.applicationId`
- Used for request headers.
- Provided by Origo.
- `Origo.replaceExistingPhotos`
- Set to true to automatically replace existing Photo ID.
- NOTE: Default is true if left unspecified.
- `Origo.usePkiAuth`
- PKI Auth is recommended for Production applications.
- This application accepts an unencrypted RSA / PEM-encoded PKCS#8 Private key.
- Service Account with certificate must be created in HID Origo Management Studio.
- `Origo.tokenUrl`
- Token URL is generated from HID Origo Management Studio when a PKI-authenticated service account is created.

### File Name Resolver Settings

- downloader.fileNameResolver
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ dependencies {
implementation 'software.amazon.awssdk:sqs:2.20.148'
implementation 'org.apache.groovy:groovy-json:4.0.15'
implementation 'org.apache.commons:commons-csv:1.8'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

implementation 'com.papertrailapp:logback-syslog4j:1.0.0'

Expand Down
16 changes: 16 additions & 0 deletions src/main/groovy/com/cloudcard/photoDownloader/AuthException.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.cloudcard.photoDownloader

class AuthException extends Exception {

public static final String MESSAGE = "Error while attempting to authenticate"

AuthException() {

super(MESSAGE);
}

AuthException(String message) {

super("$message $MESSAGE")
}
}
286 changes: 286 additions & 0 deletions src/main/groovy/com/cloudcard/photoDownloader/OrigoClient.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package com.cloudcard.photoDownloader

import io.jsonwebtoken.Jwts
import jakarta.annotation.PostConstruct
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component
import com.fasterxml.jackson.databind.ObjectMapper

import java.security.Key
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec

import static com.cloudcard.photoDownloader.ApplicationPropertiesValidator.throwIfBlank

@Component
@ConditionalOnProperty(value = "downloader.storageService", havingValue = "OrigoStorageService")
class OrigoClient {
// Makes requests to Origo API

private static final Logger log = LoggerFactory.getLogger(OrigoClient.class)

@Autowired
UnirestWrapper unirestWrapper

@Autowired
SimpleResponseLogger simpleResponseLogger

@Value('${Origo.eventManagementApi}')
String eventManagementApi

@Value('${Origo.mobileIdentitiesApi}')
String mobileIdentitiesApi

@Value('${Origo.certIdpApi}')
String certIdpApi

@Value('${Origo.organizationId}')
String organizationId

@Value('${Origo.contentType}')
String contentType

@Value('${Origo.applicationVersion}')
String applicationVersion

@Value('${Origo.applicationId}')
String applicationId

@Value('${Origo.clientId}')
String clientId

@Value('${Origo.clientSecret}')
String clientSecret

@Value('${Origo.usePkiAuth}')
boolean usePkiAuth

@Value('${Origo.tokenUrl}')
String tokenUrl

@Value('${Origo.privateKey}')
String privateKey

String accessToken

boolean isAuthenticated = false

Map<String, String> requestHeaders

@PostConstruct
init() {
throwIfBlank(eventManagementApi, "The Origo Event Management API URL must be specified.")
throwIfBlank(mobileIdentitiesApi, "The Origo Mobile Identities API URL must be specified.")
throwIfBlank(organizationId, "The Origo organization ID must be specified.")
throwIfBlank(clientSecret, "The Origo client secret must be specified.")
throwIfBlank(clientId, "The Origo client ID must be specified.")
throwIfBlank(contentType, "The Origo content-type header must be specified.")
throwIfBlank(applicationVersion, "The Origo application version must be specified.")
throwIfBlank(applicationId, "Your organization's Origo Application ID must be specified.")
throwIfBlank(usePkiAuth.toString(), "Authentication Preference must be specified.")
if (usePkiAuth) {
throwIfBlank((tokenUrl).toString(), "Token URL must be specified.")
}

log.info('=================== Initializing Origo Client ===================')
log.info(" Origo Event Management API URL : $eventManagementApi")
log.info(" Origo Mobile Identities API URL : $mobileIdentitiesApi")
log.info(" Origo organization ID : $organizationId")
log.info(" Origo content type header : $contentType")
log.info(" Origo application version : $applicationVersion")
log.info(" Origo application ID : $applicationId")
log.info(" Authentication Preference : ${usePkiAuth ? "PKI" : "Password"}")

simpleResponseLogger.source = this.class.simpleName

}

void authenticate(ResponseWrapper responseWithToken) {
// Stores token for future requests

String token = ""

if (responseWithToken.success) {
token = responseWithToken.body.access_token
accessToken = token
requestHeaders = [
'Authorization' : "Bearer $token" as String,
'Content-Type' : contentType,
'Application-Version': applicationVersion,
'Application-ID' : applicationId
]
isAuthenticated = true
} else {
log.error("Error while authenticating with Origo.")
isAuthenticated = false
}

}

ResponseWrapper makeAuthenticatedRequest(Closure request) {

ResponseWrapper response

if (!isAuthenticated && authenticate(requestAccessToken()) || isAuthenticated) {
response = request()
} else {
response = new ResponseWrapper(new AuthException())
}

response
}

String getJswt(String keyStr) {
// Input is an UNENCRYPTED RSA / PEM-encoded PKCS#8 private key.

if (!keyStr) {
throw new IllegalArgumentException("Key string not provided")
}

keyStr = keyStr.replaceAll("-----BEGIN PRIVATE KEY-----", "").replaceAll("-----END PRIVATE KEY-----", "").replaceAll("\\s", "")
.replaceAll("\\n", "").trim()

byte[] keyBytes = Base64.getDecoder().decode(keyStr)
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes)
KeyFactory keyFactory = KeyFactory.getInstance("RSA")
Key key = keyFactory.generatePrivate(keySpec)

long currentTimeMils = System.currentTimeMillis() - 5
Date currentTime = new Date(currentTimeMils)
Date expiration = new Date(currentTimeMils + 3600000)

Jwts.builder()
.subject(clientId)
.issuer(clientId)
.audience().add(tokenUrl).and()
.notBefore(currentTime)
.issuedAt(currentTime)
.expiration(expiration)
.id(UUID.randomUUID().toString())
.signWith(key)
.compact()
}

ResponseWrapper requestAccessToken() {
if (usePkiAuth) requestAccessTokenPki()
else requestAccessTokenPassword()
}

ResponseWrapper requestAccessTokenPki() {
// https://doc.origo.hidglobal.com/api/authentication/

String pkiCredentials = getJswt(privateKey)

String url = "$certIdpApi/authentication/customer/$organizationId/token"
Map<String, String> headers = ["Content-Type": "application/x-www-form-urlencoded"]
String body = "grant_type=client_credentials&client_assertion=$pkiCredentials"

ResponseWrapper response
try {
response = new ResponseWrapper(unirestWrapper.post(url, headers, body))
} catch (Exception e) {
response = new ResponseWrapper(e)
}
simpleResponseLogger.log("requestAccessTokenPki", response, "Error while authenticating with Origo.")

return response

}

ResponseWrapper requestAccessTokenPassword() {
// https://doc.origo.hidglobal.com/api/authentication/

String url = "$certIdpApi/authentication/customer/$organizationId/token"
Map<String, String> headers = ["Content-Type": "application/x-www-form-urlencoded"]
String body = "client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials"

ResponseWrapper response
try {
response = new ResponseWrapper(unirestWrapper.post(url, headers, body))
} catch (Exception e) {
response = new ResponseWrapper(e)
}
simpleResponseLogger.log("requestAccessTokenPassword", response, "Error while authenticating with Origo.")

return response

}

ResponseWrapper uploadUserPhoto(Photo photo, String fileType) {
// https://doc.origo.hidglobal.com/api/mobile-identities/#/Photo%20ID/post-customer-organization_id-users-user_id-photo
ResponseWrapper response

String url = "$mobileIdentitiesApi/customer/$organizationId/users/${photo.person.identifier}/photo"
Map<String, String> headers = requestHeaders.clone() as Map
headers["Content-Type"] = "application/vnd.assaabloy.ma.credential-management-2.2+$fileType" as String

try {
response = new ResponseWrapper(unirestWrapper.post(url, headers, photo.bytes))
} catch (Exception e) {
response = new ResponseWrapper(e)
}

simpleResponseLogger.log("uploadUserPhoto", response)

return response
}

ResponseWrapper accountPhotoApprove(String userId, String id) {
// https://doc.origo.hidglobal.com/api/mobile-identities/#/Photo%20ID/put-customer-organization_id-users-user_id-photo-photo_id-status

ResponseWrapper response

String url = "$mobileIdentitiesApi/customer/$organizationId/users/$userId/photo/$id/status"
String serializedBody = new ObjectMapper().writeValueAsString([status: "APPROVE"])

try {
response = new ResponseWrapper(unirestWrapper.put(url, requestHeaders, serializedBody))
} catch (Exception e) {
response = new ResponseWrapper(e)
}

simpleResponseLogger.log("accountPhotoApprove", response)

return response
}

ResponseWrapper getUserDetails(String userId) {
// https://doc.origo.hidglobal.com/api/mobile-identities/#/Users/get-customer-organization_id-users-user_id

ResponseWrapper response

String url = "$mobileIdentitiesApi/customer/$organizationId/users/$userId"

try {
response = new ResponseWrapper(unirestWrapper.get(url, requestHeaders))
} catch (Exception e) {
response = new ResponseWrapper(e)
}

simpleResponseLogger.log("getUserDetails", response)

return response
}

ResponseWrapper deletePhoto(String userId, String photoId) {
// https://doc.origo.hidglobal.com/api/mobile-identities/#/Photo%20ID/delete-customer-organization_id-users-user_id-photo-photo_id

ResponseWrapper response

String url = "$mobileIdentitiesApi/customer/$organizationId/users/$userId/photo/$photoId"

try {
response = new ResponseWrapper(unirestWrapper.delete(url, requestHeaders))
} catch (Exception e) {
response = new ResponseWrapper(e)
}

simpleResponseLogger.log("deletePhoto", response)

return response
}
}
Loading