diff --git a/apps/oauth2/lib/diagrams/GetOauth2TokenController.md b/apps/oauth2/lib/diagrams/GetOauth2TokenController.md new file mode 100644 index 0000000000000..8e6397d4c80d5 --- /dev/null +++ b/apps/oauth2/lib/diagrams/GetOauth2TokenController.md @@ -0,0 +1,74 @@ +```plantuml +repository Controller { + repository Oauth2Controller { + package Oauth2WebController { + // GetOauth2TokenInputPort is defined in the Features/GetOauth2Token/GetOauth2TokenPublic/GetOauth2TokenUseCase repository + class Oauth2WebController { + + Oauth2WebControllerHTTPResponse getOauth2Token(GetOauth2TokenInputPort getOauth2TokenInputPort, Oauth2WebControllerHTTPRequest oauth2WebControllerHTTPRequest) + } + + class Oauth2WebControllerHTTPRequest { + - Oauth2WebControllerHTTPRequestBody oauth2WebControllerHTTPRequestBody + + Oauth2WebControllerHTTPRequestBody getRequestBody() + + self setRequestBody(Oauth2WebControllerHTTPRequestBody oauth2WebControllerHTTPRequestBody) + } + + class Oauth2WebControllerHTTPRequestBody { + - string grant_type + - string code + - string refresh_token + - string client_id + - string client_secret + + string getGrantType() + + self setGrantType(string grant_type) + + string getCode() + + self setCode(string code) + + string getRefreshToken() + + self setRefreshToken(string refresh_token) + + string getClientId() + + self setClientId(string client_id) + + string getClientSecret() + + self setClientSecret(string client_secret) + } + + class Oauth2WebControllerHTTPResponse { + - integer response_code + - Oauth2WebControllerHTTPResponseBody oauth2WebControllerHTTPResponseBody + + integer getResponseCode() + + self setResponseCode(integer response_code) + + Oauth2WebControllerHTTPResponseBody getResponseBody() + + self setResponseBody(Oauth2WebControllerHTTPResponseBody oauth2WebControllerHTTPResponseBody) + } + + class Oauth2WebControllerHTTPResponseBody { + - string access_token + - string refresh_token + - string user_id + + string getAccessToken() + + self setAccessToken(string access_token) + + string getRefreshToken() + + self setRefreshToken(string refresh_token) + + string getUserId() + + self setUserId(string user_id) + } + + // GetOauth2TokenOutputPort is defined in the Features/GetOauth2Token/GetOauth2TokenPublic/GetOauth2TokenUseCase repository + class Oauth2WebControllerHTTPPresenter implements GetOauth2TokenOutputPort { + + void retrieveGetOauth2TokenOutputPort(GetOauth2TokenOutputPort getOauth2TokenOutputPort) + + Oauth2WebControllerHTTPResponse getHTTPResponse() + } + } + } + + respository Exceptions { + // Used by contollers' input Data Transfer Objects + class BadRequestException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + } +} +``` \ No newline at end of file diff --git a/apps/oauth2/lib/diagrams/GetOauth2TokenFeature.md b/apps/oauth2/lib/diagrams/GetOauth2TokenFeature.md new file mode 100644 index 0000000000000..fb277fd53dfce --- /dev/null +++ b/apps/oauth2/lib/diagrams/GetOauth2TokenFeature.md @@ -0,0 +1,209 @@ +```plantuml +repository Components { + package Oauth2Component { + repository Oauth2ComponentAPI { + class Oauth2ComponentAPI { + + GetOauth2TokenInputPort startOauth2TokenRetrievalInputPort() + } + } + + repository Features { + repository GetOauth2Token { + repository GetOauth2TokenPublic { + repository GetOauth2TokenUseCase { + interface GetOauth2TokenInputPort { + + void getOauth2Token(GetOauth2TokenInput, GetOauth2TokenOutputPort) + } + + class GetOauth2TokenInput { + - string grant_type + - string code + - string refresh_token + - string client_id + - string client_secret + + string getGrantType() + + self setGrantType(string grant_type) + + string getCode() + + self setCode(string code) + + string getRefreshToken() + + self setRefreshToken(string refresh_token) + + string getClientId() + + self setClientId(string client_id) + + string getClientSecret() + + self setClientSecret(string client_secret) + } + + enum GetOauth2TokenAuthorisedGrantType { + case AUTHORIZATION_CODE = "authorization_code" + case REFRESH_TOKEN = "refresh_token" + } + + class GetOauth2TokenOutput { + - string access_token + - string refresh_token + - string user_id + + string getAccessToken() + + self setAccessToken(string access_token) + + string getRefreshToken() + + self setRefreshToken(string refresh_token) + + string getUserId() + + self setUserId(string user_id) + } + + interface GetOauth2TokenOutputPort { + + void retrieveGetOauth2TokenOutputPort(GetOauth2TokenOutputPort getOauth2TokenOutputPort) + } + } + } + + repository GetOauth2TokenInternals { + repository GetOauth2TokenUseCase { + class GetOauth2TokenUseCase { + + construct( + GetOauth2TokenAccessTokenRepository getOauth2TokenAccessTokenRepository, + GetOauth2TokenClientRepository getOauth2TokenClientRepository, + GetOauth2TokenDatabaseStateHandler getOauth2TokenDatabaseStateHandler, + GetOauth2TokenCryptographyHandler getOauth2TokenCryptographyHandler, + GetOauth2TokenTimeHandler getOauth2TokenTimeHandler, + GetOauth2TokenRandomGenerator getOauth2TokenRandomGenerator, + GetOauth2TokenApplicationTokenProvider getOauth2TokenApplicationTokenProvider + ) + + void getOauth2Token(GetOauth2TokenInput getOauth2TokenInput, GetOauth2TokenOutputPort getOauth2TokenOutputPort) + } + + interface GetOauth2TokenAccessTokenRepository { + + Oauth2AccessToken getAccessTokenByCode(string oauth2_access_token_hashed_code) + + void deleteAccessToken(integer oauth2_access_token_id) + + integer rotateToken( + integer oauth2_access_token_id, + string code, + string new_code, + string new_encrypted_token, + boolean is_grant_type_authorization_code + ) + } + + interface GetOauth2TokenApplicationTokenProvider { + + ApplicationToken getTokenById(integer token_id) + + ApplicationToken rotate( + ApplicationToken applicationToken, + ApplicationToken decryptedToken, + ApplicationToken newToken + ) + + void updateToken(ApplicationToken applicationToken) + + void invalidateToken(ApplicationToken applicationToken) + } + + interface GetOauth2TokenClientRepository { + + Client getByUID(string client_UID) + } + + interface GetOauth2TokenCryptographyHandler { + + string calculateHMAC(string client_secret) + + string encrypt(string value_to_encrypt, string secret_key) + + string decrypt(string encrypted_value, string secret_key) + } + + interface GetOauth2TokenTimeHandler { + + integer getCurrentTimestamp() + } + + interface GetOauth2TokenRandomGenerator { + + string generateToken() + + string generateCode() + } + + interface GetOauth2TokenDatabaseStateHandler { + + void conserveDatabaseState() + + void revertDatabaseChanges() + + void commitDatabaseChanges() + } + } + + repository GetOauth2TokenMain { + repository Database { + class Oauth2AccessTokenDatabaseOnQBMapper implements GetOauth2TokenAccessTokenRepository { + - AccessTokenMapper + + Oauth2AccessToken getAccessTokenByCode(string oauth2_access_token_hashed_code) + + void deleteAccessToken(integer oauth2_access_token_id) + + integer rotateToken( + integer oauth2_access_token_id, + string code, + string new_code, + string new_encrypted_token, + boolean is_grant_type_authorization_code + ) + } + + class GetOauth2TokenClientMapper implements GetOauth2TokenClientRepository { + + Client getByUID(string client_UID) + } + + class GetOauth2TokenDatabaseOnMySQLHandler implements GetOauth2TokenDatabaseStateHandler { + + void conserveDatabaseState() + + void revertDatabaseChanges() + + void commitDatabaseChanges() + } + } + + repository Security { + class GetOauth2TokenInternalCryptographyHandler implements GetOauth2TokenCryptographyHandler { + + string calculateHMAC(string client_secret) + + string encrypt(string value_to_encrypt, string secret_key) + + string decrypt(string encrypted_value, string secret_key) + } + + class GetOauth2TokenInternalSecureRandomGenerator implements GetOauth2TokenRandomGenerator { + + string generateToken() + + string generateCode() + } + } + + repository Time { + class class GetOauth2TokenUtilityTimeFactory implements GetOauth2TokenTimeHandler { + + integer getCurrentTimestamp() + } + } + + repository TokenProvider { + class GetOauth2TokenTokenProvider implements GetOauth2TokenApplicationTokenProvider { + // See lib/private/Authentication/Token/PublicKeyTokenProvider.php + + Oauth2ApplicationToken getTokenById(integer token_id) + + Oauth2ApplicationToken rotate( + Oauth2ApplicationToken oauth2ApplicationToken, + string old_token_id, + string new_token_id + ) + + void updateToken(Oauth2ApplicationToken oauth2ApplicationToken) + + void invalidateToken(Oauth2ApplicationToken oauth2ApplicationToken) + } + } + } + } + } + } + } +} + +repository Exceptions { + repository UseCaseExceptions { + // Used by use cases' input Data Transfer Objects + class UnauthorisedActionException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + // Used by use cases' output Data Transfer Objects + class InternalErrorException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + } +} +``` \ No newline at end of file diff --git a/apps/oauth2/lib/diagrams/Oauth2Domain.md b/apps/oauth2/lib/diagrams/Oauth2Domain.md new file mode 100644 index 0000000000000..46c4dba0b5fbe --- /dev/null +++ b/apps/oauth2/lib/diagrams/Oauth2Domain.md @@ -0,0 +1,266 @@ +```plantuml +repository Domain { + repository Oauth2AccessToken { + class Oauth2AccessToken { + - Oauth2AccessTokenId oauth2AccessTokenId + - Oauth2AccessTokenTokenId oauth2AccessTokenTokenId + - Oauth2AccessTokenClientId oauth2AccessTokenClientId + - Oauth2AccessTokenHashedCode oauth2AccessTokenHashedCode + - Oauth2AccessTokenEncryptedToken oauth2AccessTokenEncryptedToken + - Oauth2AccessTokenCodeCreationTimestamp oauth2AccessTokenCodeCreationTimestamp + - Oauth2AccessTokenTokenCount oauth2AccessTokenTokenCount + + construct( + Oauth2AccessTokenId oauth2AccessTokenId, + Oauth2AccessTokenTokenId oauth2AccessTokenTokenId, + Oauth2AccessTokenClientId oauth2AccessTokenClientId, + Oauth2AccessTokenHashedCode oauth2AccessTokenHashedCode, + Oauth2AccessTokenEncryptedToken oauth2AccessTokenEncryptedToken, + Oauth2AccessTokenCodeCreationTimestamp oauth2AccessTokenCodeCreationTimestamp, + Oauth2AccessTokenTokenCount oauth2AccessTokenTokenCount + ) + + boolean isTokenInAuthorizationCodeState() + + boolean isTokenExpired(integer current_timestamp) + + integer retrieveId() + + integer retrieveTokenId() + + integer retrieveClientId() + + string retrieveEncryptedToken() + } + + repository ValueObjects { + class Oauth2AccessTokenId { + - integer oauth2_access_token_id + + integer getOauth2AccessTokenId() + + self setOauth2AccessTokenId(integer oauth2_access_token_id) + } + + class Oauth2AccessTokenTokenId { + - integer oauth2_access_token_token_id + + integer getOauth2AccessTokenTokenId() + + self setOauth2AccessTokenTokenId(integer oauth2_access_token_token_id) + } + + class Oauth2AccessTokenClientId { + - integer oauth2_access_token_client_id + + integer getOauth2AccessTokenClientId() + + self setOauth2AccessTokenClientId(integer oauth2_access_token_client_id) + } + + class Oauth2AccessTokenHashedCode { + - string oauth2_access_token_hashed_code + + string getOauth2AccessTokenHashedCode() + + self setOauth2AccessTokenHashedCode(string oauth2_access_token_hashed_code) + } + + class Oauth2AccessTokenEncryptedToken { + - string oauth2_access_token_encrypted_token + + string getOauth2AccessTokenEncryptedToken() + + self setOauth2AccessTokenEncryptedToken(string oauth2_access_token_encrypted_token) + } + + class Oauth2AccessTokenCodeCreationTimestamp { + - integer oauth2_access_token_code_creation_timestamp + + integer getOauth2AccessTokenCodeCreationTimestamp() + + self setOauth2AccessTokenCodeCreationTimestamp(integer oauth2_access_token_code_creation_timestamp) + } + + class Oauth2AccessTokenTokenCount { + - integer oauth2_access_token_token_count + + integer getOauth2AccessTokenTokenCount() + + self setOauth2AccessTokenTokenCount(integer oauth2_access_token_token_count) + } + } + + repository Exceptions { + repository ValueObjects { + class InvalidOauth2AccessTokenIdException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + class InvalidOauth2AccessTokenTokenIdException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + class InvalidOauth2AccessTokenClientIdException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + class InvalidOauth2AccessTokenHashedCodeException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + class InvalidOauth2AccessTokenEncryptedTokenException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + class InvalidOauth2AccessTokenCodeCreationTimestampException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + class InvalidOauth2AccessTokenTokenCountException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + // ... etc + } + } + } + + repository Client { + class Client { + - ClientSecret clientSecret + - ClientIdentifier clientIdentifier + + boolean clientIdentifierMatches(ClientIdentifier clientIdentifierToCompareTo) + + boolean clientSecretHashMatches(ClientSecret clientSecretToCompareTo) + + string retrieveClientSecret() + } + + repository ValueObjects { + class ClientSecret { + - string client_secret + + string getClientSecret() + + self setClientSecret(string client_secret) + } + + class ClientIdentifier { + - integer client_identifier + + integer getClientIdentifier() + + self setClientIdentifier(integer client_identifier) + } + } + + repository Exceptions { + repository ValueObjects { + class InvalidClientSecretException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + class InvalidClientIdentifierException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + } + } + } + + repository Oauth2ApplicationToken { + repository ValueObjects { + class Oauth2ApplicationToken { + // See lib/private/Authentication/Token/PublicKeyToken.php + - string uid + - string login_name + - string password + - string password_hash + - string name + - string token + - integer type + - integer remember + - integer last_activity + - integer last_check + - string scope + - integer expires + - string public_key + - string private_key + - integer version + - boolean is_password_valid + + string getUID() + + self setUID(string uid) + + string getLoginName() + + self setLoginName(string login_name) + + string getPassword() + + self setPassword(string password) + + string getPasswordHash() + + self setPasswordHash(string password_hash) + + string getName() + + self setName(string name) + + string getToken() + + self setToken(string token) + + integer getType() + + self setType(integer type) + + integer getRemember() + + self setRemember(integer remember) + + integer getLastActivity() + + self setLastActivity(integer last_activity) + + integer getLastCheck() + + self setLastCheck(integer last_check) + + string getScope() + + self setScope(string scope) + + integer getExpires() + + self setExpires(integer expires) + + string getPublicKey() + + self setPublicKey(string public_key) + + string getPrivateKey() + + self setPrivateKey(string private_key) + + integer getVersion() + + self setVersion(integer version) + + boolen isPasswordValid() + + self setWhetherPasswordIsValid(boolean is_password_valid) + } + } + + repository Exceptions { + repository ValueObjects { + class InvalidOauth2ApplicationTokenUIDException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + class InvalidOauth2ApplicationTokenLoginNameException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + class InvalidOauth2ApplicationTokenPasswordException { + - string error_message + - string custom_error_code + + string getErrorMessage() + + string getCustomErrorCode() + + self setCustomErrorCode(string custom_error_code) + } + + // ... etc + } + } + } +} +``` \ No newline at end of file diff --git a/apps/oauth2/lib/diagrams/readme.md b/apps/oauth2/lib/diagrams/readme.md new file mode 100644 index 0000000000000..eabf579c0257f --- /dev/null +++ b/apps/oauth2/lib/diagrams/readme.md @@ -0,0 +1,85 @@ +# Architecture Redesign + +This readme aims at helping to navigate the architecture refactoring proposition. It might be subject to further editing in order to be more explicit over the architecture redesign structure or details. + +## Inspiration + +This redesign is strongly inspired by Robert C. Martin's *Clean Architecture* book and the Packaged by Component approach presented in the *Missing Chapter*. + +## General structure + +The structure aims at a strong separation of concerns and modularity. It allows to abstract what each feature should do without them depending on low-level specifications, although it might still be tied to the general PHP programming language. + +### The "Main" layer + +The "Main" layer, presented in the **GetOauth2TokenFeature.md** file under the GetOauth2TokenInternal/GetOauth2TokenMain repository, encapsulate low-level specifications. It implements interfaces declared in the "Use Case" layer. + +### The "Controller" layer + +The "Controller" layer, presented in the **GetOauth2TokenController.md** file, is an abstraction of the runtime. In this case, the refactored architecture will present a **GetOauth2TokenWebController** that will not be tied to internal database, framework, or any kind of library. It plays the role of adapter between the runtime declared in the "Main" layer and the "Use Case" layer. + +### The "Use Case" layer + +The "Use Case" layer, presented in the **GetOauth2TokenFeature.md** file under the GetOauth2TokenPublic/GetOauth2TokenUseCase and GetOauth2TokenInternal/GetOauth2TokenUseCase repositories, is an abstraction of the business logic and validation of the feature. It specifies what the feature should do without knowing anything of its low-level implementations. + +For example, the Oauth2 token retrieval feature should: +- Check that the comunicated grant type matches the accepted grant types; +- Attempt to retrieve the AccessToken entity by informing the sent "code" parameter to a dedicated access token repository; +- Handle different feature behaviour based on what grant type was sent through the input; +- Call the AccessToken entity method to check the token authorization code state; +- etc... + +The "Use Case" layer doesn't know if its using a SQL database, a specific validation library, a 3rd-party token provider or if it runs on the web, will be used to serve a front page, a HTTP REST API Response or a command line exit code and message. + +### The "Domain" layer + +The "Domain" layer, presented in the **Oauth2Domain.md** file, is an abstraction of the core business rules. In the Oauth2 component, the Oauth2AccessToken entity exposes methods to evaluate if the token is expired and its authorization code state, the Client entity can tell if the sent client identifier or secret match its own client identifier or secret. + +Unlike the other layer, this one should aim to work accross features, enforcing validation wherever they are used. + +## Implementation details + +### Data Transfer Objects + +This architecture redesign propose to use Data Transfer Objects to validate input and output. + +#### Controller Data Transfer Objects + +The Data Tranfer Objects declared in the "Controller" layer should check that basic request and response parameters are being informed or returned without knowing in-depth validation rules. +For example: +- It should throw an exception if the request does not inform a mandatory parameter; +- It should throw an exception if the request informs an empty string for a mandatory string parameter. + +In the **GetOauth2TokenController.md** file, the Oauth2WebControllerHTTPRequest and Oauth2WebControllerHTTPResponse are Data Transfer Objects. + +#### Use Case Data Transfer Objects + +The Data Transfer Objects declared in the "Use Case" layer should check that input and ouput data match the feature validation rules, with in-depth knowledge of the feature. +For example: +- A client's name should be letters and white space only, beginning with an uppercase letter a followed by lowercase letters; +- A token should be hexadecimal, no white space, joined by hyphens. + +### Tests structure + +Unless required for good reasons, tests should work as their specific runtime and follow the full flow of data by calling the GetOauth2TokenController and pass it the GetOauth2TokenInputPort and the Oauth2WebControllerHTTPRequest. They should be "feature level" tests. + +#### Integration and Feature level tests + +As much as possible, there should be tests for: +- Features, as presented in the above paragraph; +- Integrations, in this case, testing the flow of data through the already existing OauthApiController, to check behaviour in its live runtime. + +### Repositories and classes naming conventions + +Each part of the architecture is named according to the feature it is part of, including: +- Repositories; +- Classes; +- Interfaces; +- Enums; +- etc... + +So the Oauth2 Controllers and Components expose: +- A Oauth2WebController class; +- A GetOauth2TokenPublic and GetOauth2TokenInternals repository; +- A Oauth2WebControllerHTTPResponse class; +- and more. \ No newline at end of file