├── .editorconfig ├── .githooks └── pre-commit ├── .github └── workflows │ ├── build-and-push.yml │ └── clients.yml ├── .gitignore ├── .swagger-codegen-ignore ├── .swagger-codegen └── VERSION ├── LICENSE ├── README.md ├── build.gradle ├── detekt_baseline.xml ├── detekt_config.yml ├── docker-compose.yml ├── docker └── iam-web │ ├── Dockerfile │ ├── dev.env │ └── run.sh ├── docs ├── api_reference │ ├── .openapi-generator-ignore │ ├── .openapi-generator │ │ ├── FILES │ │ └── VERSION │ ├── Apis │ │ ├── KeyManagementApi.md │ │ ├── OrganizationManagementApi.md │ │ ├── PolicyManagementApi.md │ │ ├── ResourceActionManagementApi.md │ │ ├── ResourceManagementApi.md │ │ ├── UserAuthenticationApi.md │ │ ├── UserAuthorizationApi.md │ │ ├── UserCredentialManagementApi.md │ │ ├── UserManagementApi.md │ │ ├── UserPolicyManagementApi.md │ │ └── UserVerificationApi.md │ ├── Models │ │ ├── Action.md │ │ ├── ActionPaginatedResponse.md │ │ ├── BaseSuccessResponse.md │ │ ├── ChangeUserPasswordRequest.md │ │ ├── CreateActionRequest.md │ │ ├── CreateCredentialRequest.md │ │ ├── CreateOrganizationRequest.md │ │ ├── CreateOrganizationResponse.md │ │ ├── CreatePolicyRequest.md │ │ ├── CreateResourceRequest.md │ │ ├── CreateUserRequest.md │ │ ├── Credential.md │ │ ├── CredentialWithoutSecret.md │ │ ├── ErrorResponse.md │ │ ├── GetUserPoliciesResponse.md │ │ ├── KeyResponse.md │ │ ├── ListCredentialResponse.md │ │ ├── Organization.md │ │ ├── PaginationOptions.md │ │ ├── Policy.md │ │ ├── PolicyAssociationRequest.md │ │ ├── PolicyPaginatedResponse.md │ │ ├── PolicyStatement.md │ │ ├── ResetPasswordRequest.md │ │ ├── Resource.md │ │ ├── ResourceAction.md │ │ ├── ResourceActionEffect.md │ │ ├── ResourcePaginatedResponse.md │ │ ├── RootUser.md │ │ ├── TokenResponse.md │ │ ├── UpdateActionRequest.md │ │ ├── UpdateCredentialRequest.md │ │ ├── UpdateOrganizationRequest.md │ │ ├── UpdatePolicyRequest.md │ │ ├── UpdateResourceRequest.md │ │ ├── UpdateUserRequest.md │ │ ├── User.md │ │ ├── UserPaginatedResponse.md │ │ ├── UserPolicy.md │ │ ├── ValidationRequest.md │ │ ├── ValidationResponse.md │ │ └── VerifyEmailRequest.md │ └── README.md └── docs │ └── JWT.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iam_openapi_spec.yml ├── kubernetes ├── Dockerfile └── appRunner.sh ├── settings.gradle └── src ├── main ├── kotlin │ └── com │ │ └── hypto │ │ └── iam │ │ └── server │ │ ├── Application.kt │ │ ├── Configuration.kt │ │ ├── Constants.kt │ │ ├── ErrorMessageStrings.kt │ │ ├── ExceptionHandler.kt │ │ ├── MigrationHandler.kt │ │ ├── apis │ │ ├── ActionApi.kt │ │ ├── AuthProviderApi.kt │ │ ├── CredentialApi.kt │ │ ├── KeyApi.kt │ │ ├── OrganizationApi.kt │ │ ├── PasscodeApi.kt │ │ ├── PolicyApi.kt │ │ ├── ResourceApi.kt │ │ ├── SubOrganizationApi.kt │ │ ├── TokenApi.kt │ │ ├── UserAuthApi.kt │ │ ├── UsersApi.kt │ │ └── ValidateApi.kt │ │ ├── authProviders │ │ ├── AuthProviderRegistry.kt │ │ ├── BaseAuthProvider.kt │ │ ├── GoogleAuthProvider.kt │ │ └── MicrosoftAuthProvider.kt │ │ ├── configs │ │ └── AppConfig.kt │ │ ├── db │ │ ├── listeners │ │ │ └── DeleteOrUpdateWithoutWhereListener.kt │ │ └── repositories │ │ │ ├── ActionRepo.kt │ │ │ ├── AuditEntriesRepo.kt │ │ │ ├── AuthProviderRepo.kt │ │ │ ├── BaseRepo.kt │ │ │ ├── CredentialsRepo.kt │ │ │ ├── LinkUsersRepo.kt │ │ │ ├── MasterKeysRepo.kt │ │ │ ├── OrganizationRepo.kt │ │ │ ├── PasscodeRepo.kt │ │ │ ├── PoliciesRepo.kt │ │ │ ├── PolicyTemplatesRepo.kt │ │ │ ├── PrincipalPoliciesRepo.kt │ │ │ ├── ResourceRepo.kt │ │ │ ├── SubOrganizationRepo.kt │ │ │ ├── UserAuthProvidersRepo.kt │ │ │ ├── UserAuthRepo.kt │ │ │ └── UserRepo.kt │ │ ├── di │ │ └── Modules.kt │ │ ├── exceptions │ │ ├── ApplicationExceptions.kt │ │ └── DbExceptionHandler.kt │ │ ├── extensions │ │ ├── Mappers.kt │ │ ├── Paginators.kt │ │ ├── Routing.kt │ │ ├── SubOrganizationUtils.kt │ │ ├── Utils.kt │ │ └── Validators.kt │ │ ├── idp │ │ ├── CognitoProviderImpl.kt │ │ ├── Configuration.kt │ │ └── IdentityProvider.kt │ │ ├── lambdas │ │ └── AuditEventHandler.kt │ │ ├── plugins │ │ └── globalcalldata │ │ │ ├── CallCache.kt │ │ │ ├── CallData.kt │ │ │ ├── CallDataDelegate.kt │ │ │ └── GlobalCallData.kt │ │ ├── security │ │ ├── Audit.kt │ │ ├── Authentication.kt │ │ └── Authorization.kt │ │ ├── service │ │ ├── ActionService.kt │ │ ├── AuthProviderService.kt │ │ ├── CredentialService.kt │ │ ├── DatabaseFactory.kt │ │ ├── OrganizationsService.kt │ │ ├── PasscodeService.kt │ │ ├── PolicyService.kt │ │ ├── PolicyTemplatesService.kt │ │ ├── PrincipalPolicyService.kt │ │ ├── ResourceService.kt │ │ ├── SubOrganizationService.kt │ │ ├── TokenService.kt │ │ ├── UserAuthService.kt │ │ ├── UserPrincipalService.kt │ │ ├── UsersService.kt │ │ └── ValidationService.kt │ │ ├── utils │ │ ├── ApplicationIdUtil.kt │ │ ├── EncryptUtil.kt │ │ ├── Hrn.kt │ │ ├── IamResources.kt │ │ ├── IdGenerator.kt │ │ ├── MasterKeyUtil.kt │ │ ├── Timing.kt │ │ └── policy │ │ │ ├── PolicyBuilder.kt │ │ │ ├── PolicyRequest.kt │ │ │ ├── PolicyStatement.kt │ │ │ └── PolicyValidator.kt │ │ └── validators │ │ ├── MetadataValidator.kt │ │ └── RequestModelValidator.kt └── resources │ ├── casbin_model.conf │ ├── db │ └── migration │ │ ├── V10__add_metadata_to_passcode.sql │ │ ├── V11__add_detail_columns_to_users.sql │ │ ├── V12__add_policy_template_table.sql │ │ ├── V13__add_description_to_policy_and_templates.sql │ │ ├── V14__create_auth_provider_table.sql │ │ ├── V15__create_user_auth_table.sql │ │ ├── V16__create_sub_org_table.sql │ │ ├── V17__add_required_variables_in_poly_templates.sql │ │ ├── V18__policy_template_on_create_org_flag.sql │ │ ├── V19__add_last_sent_column_in_passcode_table.sql │ │ ├── V1__init_iam_database.sql │ │ ├── V20__add_link_user_table.sql │ │ ├── V21__change_passcode_purpose_lenght.sql │ │ ├── V2__create_audit_entries_table_and_partitions.sql │ │ ├── V3__add_keys_pem_file_to_db.sql │ │ ├── V4__modify_users_table.sql │ │ ├── V5__org_rename_admin_to_root.sql │ │ ├── V6__create_passcode_table.sql │ │ ├── V7__add_preferred_username.sql │ │ ├── V8__rename_user_policies_to_principal_policies.sql │ │ └── V9__create_username_index_on_users.sql │ ├── default_config.json │ ├── generate_key_pair.sh │ └── logback.xml └── test ├── kotlin ├── com │ └── hypto │ │ └── iam │ │ └── server │ │ ├── ExceptionHandlerTest.kt │ │ ├── apis │ │ ├── ActionApiTest.kt │ │ ├── CredentialApiKtTest.kt │ │ ├── KeyApiTest.kt │ │ ├── OrganizationApiKtTest.kt │ │ ├── PasscodeApiTest.kt │ │ ├── PolicyApiTest.kt │ │ ├── ResourceApiTest.kt │ │ ├── SubOrganizationApiKtTest.kt │ │ ├── SubOrganizationUserApiTest.kt │ │ ├── TokenApiTest.kt │ │ ├── UserApiTest.kt │ │ └── UserAuthApiTest.kt │ │ ├── helpers │ │ ├── AbstractContainerBaseTest.kt │ │ ├── BaseSingleAppTest.kt │ │ ├── DataSetupHelper.kt │ │ ├── DataSetupHelperV2.kt │ │ ├── DataSetupHelperV3.kt │ │ ├── MockCognito.kt │ │ ├── MockOkHttpClient.kt │ │ ├── MockPasscodeRepo.kt │ │ ├── MockSes.kt │ │ └── PostgresInit.kt │ │ ├── utils │ │ ├── HrnTest.kt │ │ ├── IdGeneratorTest.kt │ │ ├── PolicyValidatorTest.kt │ │ └── policy │ │ │ └── PolicyBuilderTest.kt │ │ └── validators │ │ └── RequestModelValidatorTest.kt └── org │ └── jooq │ └── impl │ └── ResultImplTest.kt └── resources ├── application-custom.conf └── logback-test.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | # possible values: number (e.g. 2), "unset" (makes ktlint ignore indentation completely) 3 | indent_size=4 4 | # true (recommended) / false 5 | insert_final_newline=true 6 | # possible values: number (e.g. 120) (package name, imports & comments are ignored), "off" 7 | # it's automatically set to 100 on `ktlint --android ...` (per Android Kotlin Style Guide) 8 | max_line_length=off 9 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Running build..." 3 | OUTPUT="/tmp/detekt-$(date +%s)" 4 | ./gradlew build > $OUTPUT 5 | EXIT_CODE=$? 6 | if [ $EXIT_CODE -ne 0 ]; then 7 | cat $OUTPUT 8 | rm $OUTPUT 9 | echo "***********************************************" 10 | echo " Build failed " 11 | echo " Please fix the above issues before committing " 12 | echo "***********************************************" 13 | exit $EXIT_CODE 14 | fi 15 | rm $OUTPUT -------------------------------------------------------------------------------- /.github/workflows/clients.yml: -------------------------------------------------------------------------------- 1 | name: Client 2 | 3 | on: 4 | workflow_run: 5 | workflows: [ Build ] 6 | types: [ completed ] 7 | branches: [ main ] 8 | 9 | jobs: 10 | check: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 13 | outputs: 14 | openapi: ${{ steps.changes.outputs.openapi }} 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: File changes 18 | uses: getsentry/paths-filter@v2 19 | id: changes 20 | with: 21 | filters: | 22 | openapi: 23 | - iam_openapi_spec.yml 24 | 25 | java-client: 26 | runs-on: ubuntu-latest 27 | needs: check 28 | if: ${{ needs.check.outputs.openapi == 'true' }} 29 | steps: 30 | - uses: actions/checkout@v3 31 | with: 32 | path: iam 33 | - name: Checkout 34 | uses: actions/checkout@v3 35 | with: 36 | repository: hwslabs/iam-java-client 37 | path: client 38 | persist-credentials: false 39 | fetch-depth: 0 40 | - name: Move the spec file 41 | run: | 42 | mv iam/iam_openapi_spec.yml client/iam_openapi_spec.yml 43 | - name: Set up JDK 17 44 | uses: actions/setup-java@v2 45 | with: 46 | java-version: '17' 47 | distribution: 'temurin' 48 | - name: Generate Files 49 | working-directory: client 50 | run: | 51 | gradle --stacktrace --debug generateClient 52 | - name: Push changes 53 | uses: actions-js/push@v1.3 54 | with: 55 | github_token: ${{ secrets.CLIENT_GEN_TOKEN }} 56 | directory: client 57 | message: "Update OpenApi spec" 58 | repository: hwslabs/iam-java-client 59 | branch: main 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | .SpaceVim.d/ 4 | .vscode/ 5 | 6 | # Ignore Gradle GUI config 7 | gradle-app.setting 8 | 9 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 10 | !gradle-wrapper.jar 11 | 12 | # Cache of project 13 | .gradletasknamecache 14 | 15 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 16 | # gradle/wrapper/gradle-wrapper.properties 17 | 18 | # IDE 19 | .idea/ 20 | *.iml 21 | 22 | */.DS_Store 23 | .DS_Store 24 | 25 | docker/iam-pg/data/* 26 | docker/iam-web/gradle/* 27 | -------------------------------------------------------------------------------- /.swagger-codegen-ignore: -------------------------------------------------------------------------------- 1 | # Swagger Codegen Ignore 2 | # Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /.swagger-codegen/VERSION: -------------------------------------------------------------------------------- 1 | 3.0.32 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Wunderbaked Technologies Pvt Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /detekt_baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LongParameterList:DataSetupHelper.kt$DataSetupHelper$( orgId: String, username: String?, bearerToken: String, policyName: String, accountId: String?, resourceName: String, actionName: String, resourceInstance: String, engine: TestApplicationEngine, effect: PolicyStatement.Effect = PolicyStatement.Effect.allow ) 6 | MagicNumber:Configuration.kt$10.0 7 | MagicNumber:Configuration.kt$1024 8 | MagicNumber:Configuration.kt$365 9 | MagicNumber:Configuration.kt$60 10 | MaxLineLength:Hrn.kt$Hrn$* 11 | MaxLineLength:Hrn.kt$ResourceHrn.Companion$"""^hrn:(?<organization>[^:\n]+):(?<accountId>[^:\n]*):(?<resource>[^:/\n]*)/{0,1}(?<resourceInstance>[^/\n:]*)""".toRegex() 12 | SpreadOperator:CognitoProviderImpl.kt$CognitoIdentityProviderImpl$(emailAttr, emailVerifiedAttr, createdBy, *optionalUserAttrs.toTypedArray()) 13 | SwallowedException:Authentication.kt$e: Exception 14 | SwallowedException:PasscodeService.kt$PasscodeServiceImpl$e: EntityNotFoundException 15 | ThrowsCount:Authorization.kt$Authorization$fun interceptPipeline( pipeline: ApplicationCallPipeline, any: Set<Action>? = null, all: Set<Action>? = null, none: Set<Action>? = null, getResourceHrn: (ApplicationRequest) -> ResourceHrn ) 16 | ThrowsCount:PasscodeService.kt$PasscodeServiceImpl$private suspend fun sendInviteUserPasscode( email: String, orgId: String, passcode: String, principal: UserPrincipal ): Boolean 17 | ThrowsCount:ResourceApi.kt$fun Route.resourceApi() 18 | TooGenericExceptionCaught:Authentication.kt$e: Exception 19 | TooGenericExceptionThrown:AuditEventHandler.kt$AuditEventHandler$throw Exception("few Batch inserts failed") 20 | TooManyFunctions:BaseRepo.kt$BaseRepo<R : UpdatableRecord<R>, P, T> : KoinComponent 21 | TooManyFunctions:IdentityProvider.kt$IdentityProvider 22 | TooManyFunctions:Mappers.kt$com.hypto.iam.server.extensions.Mappers.kt 23 | TooManyFunctions:RequestModelValidator.kt$com.hypto.iam.server.validators.RequestModelValidator.kt 24 | TooManyFunctions:UsersService.kt$UsersServiceImpl : KoinComponentUsersService 25 | UnusedPrivateMember:CognitoProviderImpl.kt$CognitoIdentityProviderImpl$context: RequestContext 26 | UnusedPrivateMember:CognitoProviderImpl.kt$CognitoIdentityProviderImpl$identityGroup: IdentityGroup 27 | UnusedPrivateMember:CognitoProviderImpl.kt$CognitoIdentityProviderImpl$userCredentials: AccessTokenCredentials 28 | UnusedPrivateMember:TokenApiTest.kt$TokenApiTest.ValidateJwtToken$createdUser: RootUser 29 | 30 | 31 | -------------------------------------------------------------------------------- /detekt_config.yml: -------------------------------------------------------------------------------- 1 | style: 2 | ForbiddenComment: 3 | allowedPatterns: 'TODO:' 4 | MaxLineLength: 5 | maxLineLength: 200 6 | UseCheckOrError: 7 | active: false 8 | 9 | complexity: 10 | LongMethod: 11 | active: false 12 | LongParameterList: 13 | functionThreshold : 15 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | web: 4 | build: 5 | context: . 6 | dockerfile: ./docker/iam-web/Dockerfile 7 | volumes: 8 | - .:/iam 9 | - ./docker/iam-web/gradle:/home/gradle/.gradle 10 | ports: 11 | - "8080:8080" 12 | links: 13 | - pg 14 | depends_on: 15 | pg: 16 | condition: service_healthy 17 | command: ./docker/iam-web/run.sh 18 | env_file: 19 | - ./docker/iam-web/dev.env 20 | pg: 21 | image: postgres:14.1-alpine 22 | ports: 23 | - "4921:5432" 24 | environment: 25 | - POSTGRES_USER=root 26 | - POSTGRES_PASSWORD=password 27 | - POSTGRES_DB=iam 28 | - TZ=Asia/Kolkata 29 | volumes: 30 | - ./docker/iam-pg/data:/var/lib/postgresql/data 31 | command: ["-c", "max_connections=500"] 32 | healthcheck: 33 | test: [ "CMD", "pg_isready", "-q", "-d", "iam", "-U", "root" ] 34 | timeout: 5s 35 | interval: 5s 36 | retries: 10 37 | -------------------------------------------------------------------------------- /docker/iam-web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amazoncorretto:17.0.10-alpine 2 | RUN apk add --no-cache bash openssl 3 | FROM gradle:8-jdk17-focal 4 | WORKDIR /iam 5 | -------------------------------------------------------------------------------- /docker/iam-web/dev.env: -------------------------------------------------------------------------------- 1 | database__host=host.docker.internal 2 | database__port=4921 3 | database__username=root 4 | database__password=password 5 | -------------------------------------------------------------------------------- /docker/iam-web/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -vx 4 | 5 | gradle --stop # Stop running daemons if any 6 | 7 | ./gradlew run dockerDistJar 8 | -------------------------------------------------------------------------------- /docs/api_reference/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /docs/api_reference/.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | .openapi-generator-ignore 2 | Apis/KeyManagementApi.md 3 | Apis/OrganizationManagementApi.md 4 | Apis/PolicyManagementApi.md 5 | Apis/ResourceActionManagementApi.md 6 | Apis/ResourceManagementApi.md 7 | Apis/UserAuthenticationApi.md 8 | Apis/UserAuthorizationApi.md 9 | Apis/UserCredentialManagementApi.md 10 | Apis/UserManagementApi.md 11 | Apis/UserPolicyManagementApi.md 12 | Apis/UserVerificationApi.md 13 | Models/Action.md 14 | Models/ActionPaginatedResponse.md 15 | Models/BaseSuccessResponse.md 16 | Models/ChangeUserPasswordRequest.md 17 | Models/CreateActionRequest.md 18 | Models/CreateCredentialRequest.md 19 | Models/CreateOrganizationRequest.md 20 | Models/CreateOrganizationResponse.md 21 | Models/CreatePolicyRequest.md 22 | Models/CreateResourceRequest.md 23 | Models/CreateUserRequest.md 24 | Models/Credential.md 25 | Models/CredentialWithoutSecret.md 26 | Models/ErrorResponse.md 27 | Models/GetUserPoliciesResponse.md 28 | Models/KeyResponse.md 29 | Models/ListCredentialResponse.md 30 | Models/Organization.md 31 | Models/PaginationOptions.md 32 | Models/Policy.md 33 | Models/PolicyAssociationRequest.md 34 | Models/PolicyPaginatedResponse.md 35 | Models/PolicyStatement.md 36 | Models/ResetPasswordRequest.md 37 | Models/Resource.md 38 | Models/ResourceAction.md 39 | Models/ResourceActionEffect.md 40 | Models/ResourcePaginatedResponse.md 41 | Models/RootUser.md 42 | Models/TokenResponse.md 43 | Models/UpdateActionRequest.md 44 | Models/UpdateCredentialRequest.md 45 | Models/UpdateOrganizationRequest.md 46 | Models/UpdatePolicyRequest.md 47 | Models/UpdateResourceRequest.md 48 | Models/UpdateUserRequest.md 49 | Models/User.md 50 | Models/UserPaginatedResponse.md 51 | Models/UserPolicy.md 52 | Models/ValidationRequest.md 53 | Models/ValidationResponse.md 54 | Models/VerifyEmailRequest.md 55 | README.md 56 | -------------------------------------------------------------------------------- /docs/api_reference/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 5.3.0 -------------------------------------------------------------------------------- /docs/api_reference/Apis/KeyManagementApi.md: -------------------------------------------------------------------------------- 1 | # KeyManagementApi 2 | 3 | All URIs are relative to *https://sandbox-iam.us.hypto.com/v1* 4 | 5 | Method | HTTP request | Description 6 | ------------- | ------------- | ------------- 7 | [**getKey**](KeyManagementApi.md#getKey) | **GET** /keys/{kid} | Get keys 8 | 9 | 10 | 11 | # **getKey** 12 | > KeyResponse getKey(kid, format, type) 13 | 14 | Get keys 15 | 16 | Get public/private keys from Key-id in der/pem format 17 | 18 | ### Parameters 19 | 20 | Name | Type | Description | Notes 21 | ------------- | ------------- | ------------- | ------------- 22 | **kid** | **String**| | [default to null] 23 | **format** | **String**| | [optional] [default to pem] [enum: der, pem] 24 | **type** | **String**| | [optional] [default to public] [enum: public] 25 | 26 | ### Return type 27 | 28 | [**KeyResponse**](../Models/KeyResponse.md) 29 | 30 | ### Authorization 31 | 32 | No authorization required 33 | 34 | ### HTTP request headers 35 | 36 | - **Content-Type**: Not defined 37 | - **Accept**: application/json 38 | 39 | -------------------------------------------------------------------------------- /docs/api_reference/Apis/OrganizationManagementApi.md: -------------------------------------------------------------------------------- 1 | # OrganizationManagementApi 2 | 3 | All URIs are relative to *https://sandbox-iam.us.hypto.com/v1* 4 | 5 | Method | HTTP request | Description 6 | ------------- | ------------- | ------------- 7 | [**createOrganization**](OrganizationManagementApi.md#createOrganization) | **POST** /organizations | Creates an organization. 8 | [**deleteOrganization**](OrganizationManagementApi.md#deleteOrganization) | **DELETE** /organizations/{organization_id} | Delete an organization 9 | [**getOrganization**](OrganizationManagementApi.md#getOrganization) | **GET** /organizations/{organization_id} | Get an organization 10 | [**updateOrganization**](OrganizationManagementApi.md#updateOrganization) | **PATCH** /organizations/{organization_id} | Update an organization 11 | 12 | 13 | 14 | # **createOrganization** 15 | > CreateOrganizationResponse createOrganization(CreateOrganizationRequest) 16 | 17 | Creates an organization. 18 | 19 | Organization is the top level entity. All resources (like user, actions, policies) are created and managed under an organization. This is a privileged api and only internal applications has access to create an Organization. 20 | 21 | ### Parameters 22 | 23 | Name | Type | Description | Notes 24 | ------------- | ------------- | ------------- | ------------- 25 | **CreateOrganizationRequest** | [**CreateOrganizationRequest**](../Models/CreateOrganizationRequest.md)| Payload to create organization | [optional] 26 | 27 | ### Return type 28 | 29 | [**CreateOrganizationResponse**](../Models/CreateOrganizationResponse.md) 30 | 31 | ### Authorization 32 | 33 | [apiKeyAuth](../README.md#apiKeyAuth) 34 | 35 | ### HTTP request headers 36 | 37 | - **Content-Type**: application/json 38 | - **Accept**: application/json 39 | 40 | 41 | # **deleteOrganization** 42 | > BaseSuccessResponse deleteOrganization(organization\_id) 43 | 44 | Delete an organization 45 | 46 | Delete an organization. This is a privileged api and only internal application will have access to delete organization. 47 | 48 | ### Parameters 49 | 50 | Name | Type | Description | Notes 51 | ------------- | ------------- | ------------- | ------------- 52 | **organization\_id** | **String**| | [default to null] 53 | 54 | ### Return type 55 | 56 | [**BaseSuccessResponse**](../Models/BaseSuccessResponse.md) 57 | 58 | ### Authorization 59 | 60 | [apiKeyAuth](../README.md#apiKeyAuth) 61 | 62 | ### HTTP request headers 63 | 64 | - **Content-Type**: Not defined 65 | - **Accept**: application/json 66 | 67 | 68 | # **getOrganization** 69 | > Organization getOrganization(organization\_id) 70 | 71 | Get an organization 72 | 73 | Get an organization and the metadata for the given organization. 74 | 75 | ### Parameters 76 | 77 | Name | Type | Description | Notes 78 | ------------- | ------------- | ------------- | ------------- 79 | **organization\_id** | **String**| | [default to null] 80 | 81 | ### Return type 82 | 83 | [**Organization**](../Models/Organization.md) 84 | 85 | ### Authorization 86 | 87 | [bearerAuth](../README.md#bearerAuth) 88 | 89 | ### HTTP request headers 90 | 91 | - **Content-Type**: Not defined 92 | - **Accept**: application/json 93 | 94 | 95 | # **updateOrganization** 96 | > Organization updateOrganization(organization\_id, UpdateOrganizationRequest) 97 | 98 | Update an organization 99 | 100 | Update an organization 101 | 102 | ### Parameters 103 | 104 | Name | Type | Description | Notes 105 | ------------- | ------------- | ------------- | ------------- 106 | **organization\_id** | **String**| | [default to null] 107 | **UpdateOrganizationRequest** | [**UpdateOrganizationRequest**](../Models/UpdateOrganizationRequest.md)| Payload to update organization | 108 | 109 | ### Return type 110 | 111 | [**Organization**](../Models/Organization.md) 112 | 113 | ### Authorization 114 | 115 | [bearerAuth](../README.md#bearerAuth) 116 | 117 | ### HTTP request headers 118 | 119 | - **Content-Type**: application/json 120 | - **Accept**: application/json 121 | 122 | -------------------------------------------------------------------------------- /docs/api_reference/Apis/UserAuthenticationApi.md: -------------------------------------------------------------------------------- 1 | # UserAuthenticationApi 2 | 3 | All URIs are relative to *https://sandbox-iam.us.hypto.com/v1* 4 | 5 | Method | HTTP request | Description 6 | ------------- | ------------- | ------------- 7 | [**authenticate**](UserAuthenticationApi.md#authenticate) | **POST** /authenticate | Authenticate a request 8 | 9 | 10 | 11 | # **authenticate** 12 | > TokenResponse authenticate() 13 | 14 | Authenticate a request 15 | 16 | Authenticates the request and respond with token. Upon successful authentication, - For basic auth as well as credential based bearer auth, this API generates a token and returns it. - For JWT bearer auth, returns the same JWT token in response 17 | 18 | ### Parameters 19 | This endpoint does not need any parameter. 20 | 21 | ### Return type 22 | 23 | [**TokenResponse**](../Models/TokenResponse.md) 24 | 25 | ### Authorization 26 | 27 | [basicAuth](../README.md#basicAuth), [bearerAuth](../README.md#bearerAuth) 28 | 29 | ### HTTP request headers 30 | 31 | - **Content-Type**: Not defined 32 | - **Accept**: application/json, text/plain 33 | 34 | -------------------------------------------------------------------------------- /docs/api_reference/Apis/UserAuthorizationApi.md: -------------------------------------------------------------------------------- 1 | # UserAuthorizationApi 2 | 3 | All URIs are relative to *https://sandbox-iam.us.hypto.com/v1* 4 | 5 | Method | HTTP request | Description 6 | ------------- | ------------- | ------------- 7 | [**getToken**](UserAuthorizationApi.md#getToken) | **POST** /token | Generate a token 8 | [**getTokenForOrg**](UserAuthorizationApi.md#getTokenForOrg) | **POST** /organizations/{organization_id}/token | Generate a organization_id scoped token 9 | [**validate**](UserAuthorizationApi.md#validate) | **POST** /validate | Validate an auth request 10 | 11 | 12 | 13 | # **getToken** 14 | > TokenResponse getToken() 15 | 16 | Generate a token 17 | 18 | Generate a token for the given user credential (same as /organizations/{organization_id}/token at the moment. Might change in future) 19 | 20 | ### Parameters 21 | This endpoint does not need any parameter. 22 | 23 | ### Return type 24 | 25 | [**TokenResponse**](../Models/TokenResponse.md) 26 | 27 | ### Authorization 28 | 29 | [basicAuth](../README.md#basicAuth), [bearerAuth](../README.md#bearerAuth) 30 | 31 | ### HTTP request headers 32 | 33 | - **Content-Type**: Not defined 34 | - **Accept**: application/json, text/plain 35 | 36 | 37 | # **getTokenForOrg** 38 | > TokenResponse getTokenForOrg(organization\_id) 39 | 40 | Generate a organization_id scoped token 41 | 42 | Generate a token for the given user credential scoped by the provided organization_id 43 | 44 | ### Parameters 45 | 46 | Name | Type | Description | Notes 47 | ------------- | ------------- | ------------- | ------------- 48 | **organization\_id** | **String**| | [default to null] 49 | 50 | ### Return type 51 | 52 | [**TokenResponse**](../Models/TokenResponse.md) 53 | 54 | ### Authorization 55 | 56 | [basicAuth](../README.md#basicAuth), [bearerAuth](../README.md#bearerAuth) 57 | 58 | ### HTTP request headers 59 | 60 | - **Content-Type**: Not defined 61 | - **Accept**: application/json, text/plain 62 | 63 | 64 | # **validate** 65 | > ValidationResponse validate(ValidationRequest) 66 | 67 | Validate an auth request 68 | 69 | Validate if the caller has access to resource-action in the request 70 | 71 | ### Parameters 72 | 73 | Name | Type | Description | Notes 74 | ------------- | ------------- | ------------- | ------------- 75 | **ValidationRequest** | [**ValidationRequest**](../Models/ValidationRequest.md)| Payload to validate if a user has access to a resource-action | 76 | 77 | ### Return type 78 | 79 | [**ValidationResponse**](../Models/ValidationResponse.md) 80 | 81 | ### Authorization 82 | 83 | [bearerAuth](../README.md#bearerAuth) 84 | 85 | ### HTTP request headers 86 | 87 | - **Content-Type**: application/json 88 | - **Accept**: application/json 89 | 90 | -------------------------------------------------------------------------------- /docs/api_reference/Apis/UserPolicyManagementApi.md: -------------------------------------------------------------------------------- 1 | # UserPolicyManagementApi 2 | 3 | All URIs are relative to *https://sandbox-iam.us.hypto.com/v1* 4 | 5 | Method | HTTP request | Description 6 | ------------- | ------------- | ------------- 7 | [**attachPolicies**](UserPolicyManagementApi.md#attachPolicies) | **PATCH** /organizations/{organization_id}/users/{user_name}/attach_policies | Attach policies to user 8 | [**detachPolicies**](UserPolicyManagementApi.md#detachPolicies) | **PATCH** /organizations/{organization_id}/users/{user_name}/detach_policies | Detach policies from user 9 | [**getUserPolicies**](UserPolicyManagementApi.md#getUserPolicies) | **GET** /organizations/{organization_id}/users/{user_name}/policies | List all policies associated with user 10 | 11 | 12 | 13 | # **attachPolicies** 14 | > BaseSuccessResponse attachPolicies(user\_name, organization\_id, PolicyAssociationRequest) 15 | 16 | Attach policies to user 17 | 18 | Attach policies to user 19 | 20 | ### Parameters 21 | 22 | Name | Type | Description | Notes 23 | ------------- | ------------- | ------------- | ------------- 24 | **user\_name** | **String**| | [default to null] 25 | **organization\_id** | **String**| | [default to null] 26 | **PolicyAssociationRequest** | [**PolicyAssociationRequest**](../Models/PolicyAssociationRequest.md)| Payload to attach / detach a policy to a user / resource | 27 | 28 | ### Return type 29 | 30 | [**BaseSuccessResponse**](../Models/BaseSuccessResponse.md) 31 | 32 | ### Authorization 33 | 34 | [bearerAuth](../README.md#bearerAuth) 35 | 36 | ### HTTP request headers 37 | 38 | - **Content-Type**: application/json 39 | - **Accept**: application/json 40 | 41 | 42 | # **detachPolicies** 43 | > BaseSuccessResponse detachPolicies(user\_name, organization\_id, PolicyAssociationRequest) 44 | 45 | Detach policies from user 46 | 47 | Detach policies from user 48 | 49 | ### Parameters 50 | 51 | Name | Type | Description | Notes 52 | ------------- | ------------- | ------------- | ------------- 53 | **user\_name** | **String**| | [default to null] 54 | **organization\_id** | **String**| | [default to null] 55 | **PolicyAssociationRequest** | [**PolicyAssociationRequest**](../Models/PolicyAssociationRequest.md)| Payload to attach / detach a policy to a user / resource | 56 | 57 | ### Return type 58 | 59 | [**BaseSuccessResponse**](../Models/BaseSuccessResponse.md) 60 | 61 | ### Authorization 62 | 63 | [bearerAuth](../README.md#bearerAuth) 64 | 65 | ### HTTP request headers 66 | 67 | - **Content-Type**: application/json 68 | - **Accept**: application/json 69 | 70 | 71 | # **getUserPolicies** 72 | > PolicyPaginatedResponse getUserPolicies(user\_name, organization\_id, nextToken, pageSize, sortOrder) 73 | 74 | List all policies associated with user 75 | 76 | List all policies associated with user 77 | 78 | ### Parameters 79 | 80 | Name | Type | Description | Notes 81 | ------------- | ------------- | ------------- | ------------- 82 | **user\_name** | **String**| | [default to null] 83 | **organization\_id** | **String**| | [default to null] 84 | **nextToken** | **String**| | [optional] [default to null] 85 | **pageSize** | **String**| | [optional] [default to null] 86 | **sortOrder** | **String**| | [optional] [default to null] [enum: asc, desc] 87 | 88 | ### Return type 89 | 90 | [**PolicyPaginatedResponse**](../Models/PolicyPaginatedResponse.md) 91 | 92 | ### Authorization 93 | 94 | [bearerAuth](../README.md#bearerAuth) 95 | 96 | ### HTTP request headers 97 | 98 | - **Content-Type**: Not defined 99 | - **Accept**: application/json 100 | 101 | -------------------------------------------------------------------------------- /docs/api_reference/Apis/UserVerificationApi.md: -------------------------------------------------------------------------------- 1 | # UserVerificationApi 2 | 3 | All URIs are relative to *https://sandbox-iam.us.hypto.com/v1* 4 | 5 | Method | HTTP request | Description 6 | ------------- | ------------- | ------------- 7 | [**verifyEmail**](UserVerificationApi.md#verifyEmail) | **POST** /verifyEmail | Verify email 8 | 9 | 10 | 11 | # **verifyEmail** 12 | > BaseSuccessResponse verifyEmail(VerifyEmailRequest) 13 | 14 | Verify email 15 | 16 | Verify email during account opening and resetting password 17 | 18 | ### Parameters 19 | 20 | Name | Type | Description | Notes 21 | ------------- | ------------- | ------------- | ------------- 22 | **VerifyEmailRequest** | [**VerifyEmailRequest**](../Models/VerifyEmailRequest.md)| Payload to send verification link to email | 23 | 24 | ### Return type 25 | 26 | [**BaseSuccessResponse**](../Models/BaseSuccessResponse.md) 27 | 28 | ### Authorization 29 | 30 | No authorization required 31 | 32 | ### HTTP request headers 33 | 34 | - **Content-Type**: application/json 35 | - **Accept**: application/json 36 | 37 | -------------------------------------------------------------------------------- /docs/api_reference/Models/Action.md: -------------------------------------------------------------------------------- 1 | # Action 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **organizationId** | **String** | | [default to null] 7 | **resourceName** | **String** | | [default to null] 8 | **name** | **String** | | [default to null] 9 | **hrn** | **String** | | [default to null] 10 | **description** | **String** | | [optional] [default to null] 11 | 12 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 13 | 14 | -------------------------------------------------------------------------------- /docs/api_reference/Models/ActionPaginatedResponse.md: -------------------------------------------------------------------------------- 1 | # ActionPaginatedResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **data** | [**List**](Action.md) | | [optional] [default to null] 7 | **nextToken** | **String** | | [optional] [default to null] 8 | **context** | [**PaginationOptions**](PaginationOptions.md) | | [optional] [default to null] 9 | 10 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 11 | 12 | -------------------------------------------------------------------------------- /docs/api_reference/Models/BaseSuccessResponse.md: -------------------------------------------------------------------------------- 1 | # BaseSuccessResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **success** | **Boolean** | | [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/ChangeUserPasswordRequest.md: -------------------------------------------------------------------------------- 1 | # ChangeUserPasswordRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **oldPassword** | **String** | | [default to null] 7 | **newPassword** | **String** | | [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/CreateActionRequest.md: -------------------------------------------------------------------------------- 1 | # CreateActionRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **name** | **String** | | [default to null] 7 | **description** | **String** | | [optional] [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/CreateCredentialRequest.md: -------------------------------------------------------------------------------- 1 | # CreateCredentialRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **validUntil** | **String** | | [optional] [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/CreateOrganizationRequest.md: -------------------------------------------------------------------------------- 1 | # CreateOrganizationRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **name** | **String** | | [default to null] 7 | **description** | **String** | | [optional] [default to null] 8 | **rootUser** | [**RootUser**](RootUser.md) | | [default to null] 9 | 10 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 11 | 12 | -------------------------------------------------------------------------------- /docs/api_reference/Models/CreateOrganizationResponse.md: -------------------------------------------------------------------------------- 1 | # CreateOrganizationResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **organization** | [**Organization**](Organization.md) | | [default to null] 7 | **rootUserToken** | **String** | JWT token of the root user | [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/CreatePolicyRequest.md: -------------------------------------------------------------------------------- 1 | # CreatePolicyRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **name** | **String** | | [default to null] 7 | **statements** | [**List**](PolicyStatement.md) | | [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/CreateResourceRequest.md: -------------------------------------------------------------------------------- 1 | # CreateResourceRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **name** | **String** | | [default to null] 7 | **description** | **String** | | [optional] [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/CreateUserRequest.md: -------------------------------------------------------------------------------- 1 | # CreateUserRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **preferredUsername** | **String** | | [optional] [default to null] 7 | **name** | **String** | | [default to null] 8 | **password** | **String** | | [default to null] 9 | **email** | **String** | | [default to null] 10 | **phone** | **String** | | [optional] [default to null] 11 | **status** | **String** | | [default to null] 12 | **verified** | **Boolean** | | [optional] [default to null] 13 | 14 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 15 | 16 | -------------------------------------------------------------------------------- /docs/api_reference/Models/Credential.md: -------------------------------------------------------------------------------- 1 | # Credential 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **id** | **String** | | [default to null] 7 | **validUntil** | **String** | | [optional] [default to null] 8 | **status** | **String** | | [default to null] 9 | **secret** | **String** | | [default to null] 10 | **createdAt** | **Date** | | [default to null] 11 | **updatedAt** | **Date** | | [default to null] 12 | 13 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 14 | 15 | -------------------------------------------------------------------------------- /docs/api_reference/Models/CredentialWithoutSecret.md: -------------------------------------------------------------------------------- 1 | # CredentialWithoutSecret 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **id** | **String** | | [default to null] 7 | **validUntil** | **String** | | [optional] [default to null] 8 | **status** | **String** | | [default to null] 9 | **createdAt** | **Date** | | [default to null] 10 | **updatedAt** | **Date** | | [default to null] 11 | 12 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 13 | 14 | -------------------------------------------------------------------------------- /docs/api_reference/Models/ErrorResponse.md: -------------------------------------------------------------------------------- 1 | # ErrorResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **message** | **String** | error description | [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/GetUserPoliciesResponse.md: -------------------------------------------------------------------------------- 1 | # GetUserPoliciesResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **policies** | [**List**](UserPolicy.md) | | [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/KeyResponse.md: -------------------------------------------------------------------------------- 1 | # KeyResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **kid** | **String** | | [default to null] 7 | **status** | **String** | The status of the key. Valid values are SIGNING, VERIFYING and EXPIRED | [default to null] 8 | **format** | **String** | | [default to null] 9 | **key** | **String** | | [default to null] 10 | 11 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 12 | 13 | -------------------------------------------------------------------------------- /docs/api_reference/Models/ListCredentialResponse.md: -------------------------------------------------------------------------------- 1 | # ListCredentialResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **credentials** | [**List**](CredentialWithoutSecret.md) | | [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/Organization.md: -------------------------------------------------------------------------------- 1 | # Organization 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **id** | **String** | | [default to null] 7 | **name** | **String** | | [default to null] 8 | **description** | **String** | | [optional] [default to null] 9 | **rootUser** | [**User**](User.md) | | [default to null] 10 | **createdAt** | **Date** | | [default to null] 11 | **updatedAt** | **Date** | | [default to null] 12 | 13 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 14 | 15 | -------------------------------------------------------------------------------- /docs/api_reference/Models/PaginationOptions.md: -------------------------------------------------------------------------------- 1 | # PaginationOptions 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **pageSize** | **Integer** | | [optional] [default to null] 7 | **sortOrder** | **String** | | [optional] [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/Policy.md: -------------------------------------------------------------------------------- 1 | # Policy 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **name** | **String** | | [default to null] 7 | **organizationId** | **String** | | [default to null] 8 | **hrn** | **String** | | [default to null] 9 | **version** | **Integer** | | [default to null] 10 | **statements** | [**List**](PolicyStatement.md) | | [default to null] 11 | 12 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 13 | 14 | -------------------------------------------------------------------------------- /docs/api_reference/Models/PolicyAssociationRequest.md: -------------------------------------------------------------------------------- 1 | # PolicyAssociationRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **policies** | **List** | | [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/PolicyPaginatedResponse.md: -------------------------------------------------------------------------------- 1 | # PolicyPaginatedResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **data** | [**List**](Policy.md) | | [optional] [default to null] 7 | **nextToken** | **String** | | [optional] [default to null] 8 | **context** | [**PaginationOptions**](PaginationOptions.md) | | [optional] [default to null] 9 | 10 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 11 | 12 | -------------------------------------------------------------------------------- /docs/api_reference/Models/PolicyStatement.md: -------------------------------------------------------------------------------- 1 | # PolicyStatement 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **resource** | **String** | | [default to null] 7 | **action** | **String** | | [default to null] 8 | **effect** | **String** | | [default to null] 9 | 10 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 11 | 12 | -------------------------------------------------------------------------------- /docs/api_reference/Models/ResetPasswordRequest.md: -------------------------------------------------------------------------------- 1 | # ResetPasswordRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **email** | **String** | | [default to null] 7 | **password** | **String** | | [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/Resource.md: -------------------------------------------------------------------------------- 1 | # Resource 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **name** | **String** | | [default to null] 7 | **organizationId** | **String** | | [default to null] 8 | **hrn** | **String** | | [default to null] 9 | **description** | **String** | | [optional] [default to null] 10 | 11 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 12 | 13 | -------------------------------------------------------------------------------- /docs/api_reference/Models/ResourceAction.md: -------------------------------------------------------------------------------- 1 | # ResourceAction 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **resource** | **String** | | [default to null] 7 | **action** | **String** | | [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/ResourceActionEffect.md: -------------------------------------------------------------------------------- 1 | # ResourceActionEffect 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **resource** | **String** | | [default to null] 7 | **action** | **String** | | [default to null] 8 | **effect** | **String** | | [default to null] 9 | 10 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 11 | 12 | -------------------------------------------------------------------------------- /docs/api_reference/Models/ResourcePaginatedResponse.md: -------------------------------------------------------------------------------- 1 | # ResourcePaginatedResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **data** | [**List**](Resource.md) | | [optional] [default to null] 7 | **nextToken** | **String** | | [optional] [default to null] 8 | **context** | [**PaginationOptions**](PaginationOptions.md) | | [optional] [default to null] 9 | 10 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 11 | 12 | -------------------------------------------------------------------------------- /docs/api_reference/Models/RootUser.md: -------------------------------------------------------------------------------- 1 | # RootUser 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **preferredUsername** | **String** | | [optional] [default to null] 7 | **name** | **String** | | [optional] [default to null] 8 | **password** | **String** | | [default to null] 9 | **email** | **String** | | [default to null] 10 | **phone** | **String** | | [optional] [default to null] 11 | 12 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 13 | 14 | -------------------------------------------------------------------------------- /docs/api_reference/Models/TokenResponse.md: -------------------------------------------------------------------------------- 1 | # TokenResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **token** | **String** | | [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/UpdateActionRequest.md: -------------------------------------------------------------------------------- 1 | # UpdateActionRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **description** | **String** | | [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/UpdateCredentialRequest.md: -------------------------------------------------------------------------------- 1 | # UpdateCredentialRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **validUntil** | **String** | | [optional] [default to null] 7 | **status** | **String** | | [optional] [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/UpdateOrganizationRequest.md: -------------------------------------------------------------------------------- 1 | # UpdateOrganizationRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **name** | **String** | | [optional] [default to null] 7 | **description** | **String** | | [optional] [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/UpdatePolicyRequest.md: -------------------------------------------------------------------------------- 1 | # UpdatePolicyRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **statements** | [**List**](PolicyStatement.md) | | [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/UpdateResourceRequest.md: -------------------------------------------------------------------------------- 1 | # UpdateResourceRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **description** | **String** | | [optional] [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/UpdateUserRequest.md: -------------------------------------------------------------------------------- 1 | # UpdateUserRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **name** | **String** | | [optional] [default to null] 7 | **email** | **String** | | [optional] [default to null] 8 | **phone** | **String** | | [optional] [default to null] 9 | **status** | **String** | | [optional] [default to null] 10 | **verified** | **Boolean** | | [optional] [default to null] 11 | 12 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 13 | 14 | -------------------------------------------------------------------------------- /docs/api_reference/Models/User.md: -------------------------------------------------------------------------------- 1 | # User 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **hrn** | **String** | | [default to null] 7 | **username** | **String** | | [default to null] 8 | **preferredUsername** | **String** | | [optional] [default to null] 9 | **name** | **String** | | [default to null] 10 | **organizationId** | **String** | | [default to null] 11 | **email** | **String** | | [default to null] 12 | **status** | **String** | | [default to null] 13 | **phone** | **String** | | [optional] [default to null] 14 | **loginAccess** | **Boolean** | | [optional] [default to null] 15 | **createdBy** | **String** | | [optional] [default to null] 16 | **verified** | **Boolean** | | [default to null] 17 | 18 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 19 | 20 | -------------------------------------------------------------------------------- /docs/api_reference/Models/UserPaginatedResponse.md: -------------------------------------------------------------------------------- 1 | # UserPaginatedResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **data** | [**List**](User.md) | | [optional] [default to null] 7 | **nextToken** | **String** | | [optional] [default to null] 8 | **context** | [**PaginationOptions**](PaginationOptions.md) | | [optional] [default to null] 9 | 10 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 11 | 12 | -------------------------------------------------------------------------------- /docs/api_reference/Models/UserPolicy.md: -------------------------------------------------------------------------------- 1 | # UserPolicy 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **name** | **String** | | [default to null] 7 | **organizationId** | **String** | | [default to null] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/api_reference/Models/ValidationRequest.md: -------------------------------------------------------------------------------- 1 | # ValidationRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **validations** | [**List**](ResourceAction.md) | | [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/ValidationResponse.md: -------------------------------------------------------------------------------- 1 | # ValidationResponse 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **results** | [**List**](ResourceActionEffect.md) | | [default to null] 7 | 8 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 9 | 10 | -------------------------------------------------------------------------------- /docs/api_reference/Models/VerifyEmailRequest.md: -------------------------------------------------------------------------------- 1 | # VerifyEmailRequest 2 | ## Properties 3 | 4 | Name | Type | Description | Notes 5 | ------------ | ------------- | ------------- | ------------- 6 | **email** | **String** | | [default to null] 7 | **organizationId** | **String** | | [optional] [default to null] 8 | **purpose** | **String** | | [default to null] 9 | **metadata** | **Map** | Additional metadata to be sent along with the request. Every purpose requires different metadata. - signup : if user provides admin user and org details in metadata, they don't need to be provided in the request body during CreateOrganization request. Supported metadata keys: 1. name : string (required): name of the organization 2. description : string (optional) - description of the organization 3. rootUserPassword : string (required) - password of the root user 4. rootUserName : string (optional) - name of the root user 5. rootUserPreferredUsername : string (optional) - preferred username of the root user 6. rootUserPhone : string (optional) - phone number of the root user | [optional] [default to null] 10 | 11 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 12 | 13 | -------------------------------------------------------------------------------- /docs/docs/JWT.md: -------------------------------------------------------------------------------- 1 | # IAM JWT Token 2 | 3 | ## Structure 4 | 5 | The structure of a JWT token is as what is specified in [RFC7519](https://www.rfc-editor.org/rfc/rfc7519.html). 6 | 7 | 8 | **Header:** 9 | 10 | **kid** - This field contains the key-id which was used to sign the JWT token. This is further explained in the Signature section. 11 | 12 | **zip** - This field contains the algorithm which was used to compress the body. 13 | Further info in [Notes on Compression](JWT.md#notes-on-compression) section. 14 | 15 | ```json5 16 | { 17 | "kid": "aa5db89b-112b-4d58-918b-f7e485cf3e47", 18 | "alg": "ES256", 19 | "zip": "GZIP" // Indicates the algorithm used to compress the body 20 | } 21 | ``` 22 | 23 | **Body:** 24 | 25 | The **entitlements** claim contains all the permissions that the user has in 26 | [**Casbin policy definition**](https://casbin.org/docs/en/syntax-for-models#policy-definition) format. 27 | 28 | ```json5 29 | { 30 | "iss": "https://iam.hypto.com", 31 | "iat": 1647500446, //epoch seconds 32 | "exp": 1647500746, //epoch seconds 33 | "ver": "1.0", 34 | "usr": "hrn:N1nEvjKSCr::iam-user/admin", //user HRN 35 | "org": "N1nEvjKSCr", // Organization id 36 | "entitlements": "p, hrn:N1nEvjKSCr::iam-policy/admin, N1nEvjKSCr, hrn:N1nEvjKSCr:*, allow\n\ng, hrn:N1nEvjKSCr::iam-user/admin, hrn:N1nEvjKSCr::iam-policy/admin\n" 37 | } 38 | ``` 39 | 40 | **Signature:** 41 | IAM maintains private and public key pair to sign the JWT tokens which are rotated as a best practice. 42 | Each key pair is identifiable using a key-id which is sent as part of JWT header. In the future, clients can use this 43 | identifier to request the respective public key in order to verify the signature. 44 | 45 | #### Notes on Compression 46 | 47 | IAM uses the JWS compression feature provided by [jjwt library](https://github.com/jwtk/jjwt) to create and parse JWT tokens. 48 | This feature might not be available in many other libraries as JWT specification only standardizes this feature for 49 | JWEs (Encrypted JWTs) and not JWSs (Signed JWTs) as mentioned [here](https://github.com/jwtk/jjwt#compression). 50 | But, this is used by IAM to increase the number of permissions that can be assigned to users. In the future, 51 | IAM might use JWEs instead of JWTs to abide by specification. -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.parallel=true 3 | org.gradle.jvmargs=-XX:+UseParallelGC -Xmx3g -Dkotlin.daemon.jvm.options=-Xmx2g 4 | org.gradle.unsafe.configuration-cache=false 5 | 6 | # gradle.properties for debugging 7 | kotlin.build.report.enable=true 8 | kotlin.build.report.verbose=true 9 | 10 | # gradle property for metrics 11 | kotlin.build.report.metrics=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hwslabs/iam/67b80da0543e40b3f1fb54177dd4c3662f22e9b8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /kubernetes/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amazoncorretto:17.0.10-alpine 2 | 3 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup 4 | 5 | RUN apk add --no-cache bash openssl 6 | 7 | RUN mkdir -p /opt/app 8 | RUN mkdir -p /opt/app_logs 9 | 10 | WORKDIR /opt/app 11 | 12 | ENV LOG_DEST="/opt/app_logs" 13 | 14 | EXPOSE 8080 15 | 16 | COPY /build/libs/hypto-iam-server-1.0.0-all.jar /opt/app/app.jar 17 | 18 | COPY /kubernetes/appRunner.sh /opt/app/appRunner.sh 19 | 20 | RUN chown -R appuser:appgroup /opt/app 21 | RUN chown -R appuser:appgroup /opt/app_logs 22 | 23 | USER appuser 24 | 25 | CMD sh -c /opt/app/appRunner.sh 26 | -------------------------------------------------------------------------------- /kubernetes/appRunner.sh: -------------------------------------------------------------------------------- 1 | # Option e: Exit immediately if a command exits with a non-zero status 2 | set -e 3 | 4 | java -cp /opt/app/app.jar com.hypto.iam.server.MigrationHandler 5 | java -Xlog:gc*=debug:file=/opt/app_logs/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/app_logs/heap-dumps -XX:MaxMetaspaceSize=398928K -XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0 -jar /opt/app/app.jar 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'hypto-iam-server' -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server 2 | 3 | import com.hypto.iam.server.models.PaginationOptions 4 | 5 | class Constants private constructor() { 6 | // Validation constants 7 | companion object { 8 | const val MIN_LENGTH = 2 9 | const val MIN_USERNAME_LENGTH = 8 10 | const val MIN_EMAIL_LENGTH = 4 11 | const val MAX_NAME_LENGTH = 50 12 | const val MAX_SUB_ORG_LENGTH = 1200 13 | const val MAX_USERNAME_LENGTH = 50 14 | const val MIN_DESC_LENGTH = 2 15 | const val MAX_DESC_LENGTH = 100 16 | const val MIN_POLICY_STATEMENTS = 1 17 | const val MAX_POLICY_STATEMENTS = 50 18 | const val MAX_POLICY_ASSOCIATIONS_PER_REQUEST = 20 19 | const val MINIMUM_PHONE_NUMBER_LENGTH = 8 20 | const val MINIMUM_PASSWORD_LENGTH = 8 21 | 22 | const val PAGINATION_MAX_PAGE_SIZE = 50 23 | const val PAGINATION_DEFAULT_PAGE_SIZE = 50 24 | val PAGINATION_DEFAULT_SORT_ORDER = PaginationOptions.SortOrder.asc 25 | const val NEWRELIC_METRICS_PUBLISH_INTERVAL = 30L // seconds 26 | const val X_ORGANIZATION_HEADER = "X-Iam-User-Organization" 27 | const val X_API_KEY_HEADER = "X-Api-Key" 28 | const val AUTHORIZATION_HEADER = "Authorization" 29 | const val SECRET_PREFIX = "$" 30 | const val JOOQ_QUERY_NAME = "queryName" 31 | const val SECONDS_IN_DAY = 24 * 60 * 60L 32 | 33 | const val POLICY_NAME = "policy_name" 34 | const val ORGANIZATION_ID_KEY = "organization_id" 35 | const val SUB_ORGANIZATION_ID_KEY = "sub_organization_id" 36 | const val USER_HRN_KEY = "user_hrn" 37 | const val USER_ID = "user_id" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/ErrorMessageStrings.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server 2 | 3 | object ErrorMessageStrings { 4 | // JWT Validation Errors 5 | const val JWT_INVALID_ISSUER = "Invalid Issuer in JWT" 6 | const val JWT_INVALID_USER_HRN = "Invalid User HRN in JWT" 7 | const val JWT_INVALID_ORGANIZATION = "Organization not present in JWT" 8 | const val JWT_INVALID_VERSION_NUMBER = "Version Number not present in JWT" 9 | const val JWT_INVALID_ISSUED_AT = "JWT Issued At date - %s is invalid" 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/MigrationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server 2 | 3 | import com.hypto.iam.server.configs.AppConfig 4 | import com.hypto.iam.server.di.applicationModule 5 | import kotlinx.coroutines.runBlocking 6 | import org.flywaydb.core.Flyway 7 | import org.koin.core.component.KoinComponent 8 | import org.koin.core.component.inject 9 | import org.koin.core.context.startKoin 10 | import org.koin.logger.SLF4JLogger 11 | import java.util.TimeZone 12 | 13 | class MigrationHandler : KoinComponent { 14 | private val appConfig: AppConfig by inject() 15 | 16 | companion object { 17 | @JvmStatic fun main(args: Array) { 18 | TimeZone.setDefault(TimeZone.getTimeZone("UTC")) 19 | 20 | startKoin { 21 | SLF4JLogger() 22 | modules(arrayListOf(applicationModule)) 23 | } 24 | 25 | val migrationHandler = MigrationHandler() 26 | runBlocking { migrationHandler.migrate() } 27 | } 28 | } 29 | 30 | private fun migrate() { 31 | val flyway = 32 | Flyway.configure() 33 | .dataSource( 34 | appConfig.database.jdbcUrl, 35 | appConfig.database.username, 36 | appConfig.database.password, 37 | ).load() 38 | flyway.migrate() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/apis/AuthProviderApi.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.apis 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.extensions.PaginationContext 5 | import com.hypto.iam.server.service.AuthProviderService 6 | import io.ktor.http.ContentType 7 | import io.ktor.http.HttpStatusCode 8 | import io.ktor.server.application.call 9 | import io.ktor.server.response.respondText 10 | import io.ktor.server.routing.Route 11 | import io.ktor.server.routing.get 12 | import org.koin.ktor.ext.inject 13 | 14 | fun Route.authProviderApi() { 15 | val authProviderService: AuthProviderService by inject() 16 | val gson: Gson by inject() 17 | 18 | get("/auth_providers") { 19 | val nextToken = call.request.queryParameters["next_token"] 20 | val pageSize = call.request.queryParameters["page_size"] 21 | 22 | val paginationContext = 23 | PaginationContext.from( 24 | nextToken, 25 | pageSize?.toInt(), 26 | null, 27 | ) 28 | 29 | val response = authProviderService.listAuthProvider(paginationContext) 30 | call.respondText( 31 | text = gson.toJson(response), 32 | contentType = ContentType.Application.Json, 33 | status = HttpStatusCode.OK, 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/apis/KeyApi.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.apis 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.models.KeyResponse 5 | import com.hypto.iam.server.service.MasterKey 6 | import io.ktor.http.ContentType 7 | import io.ktor.http.HttpStatusCode 8 | import io.ktor.server.application.call 9 | import io.ktor.server.response.respondText 10 | import io.ktor.server.routing.Route 11 | import io.ktor.server.routing.get 12 | import org.koin.ktor.ext.inject 13 | import java.util.Base64 14 | 15 | fun Route.keyApi() { 16 | val gson: Gson by inject() 17 | 18 | get("/keys/{kid}") { 19 | val kid = call.parameters["kid"] 20 | val format = call.request.queryParameters["format"] ?: "pem" 21 | val type = call.request.queryParameters["type"] ?: "public" 22 | 23 | require(type == "public") { "Only public key is supported" } 24 | 25 | val masterKey = MasterKey.of(kid!!) 26 | val key = 27 | when (format) { 28 | "der" -> masterKey.publicKeyDer 29 | "pem" -> masterKey.publicKeyPem 30 | else -> { 31 | throw IllegalArgumentException("Invalid format") 32 | } 33 | } 34 | 35 | val response = 36 | KeyResponse( 37 | kid, 38 | masterKey.status.toString(), 39 | KeyResponse.Format.valueOf(format), 40 | Base64.getEncoder().encodeToString(key), 41 | ) 42 | 43 | call.respondText( 44 | text = gson.toJson(response), 45 | contentType = ContentType.Application.Json, 46 | status = HttpStatusCode.OK, 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/apis/UserAuthApi.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.apis 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.models.AddUserAuthMethodRequest 5 | import com.hypto.iam.server.security.UserPrincipal 6 | import com.hypto.iam.server.security.getAuthorizationDetails 7 | import com.hypto.iam.server.security.withPermission 8 | import com.hypto.iam.server.service.UserAuthService 9 | import com.hypto.iam.server.validators.validate 10 | import io.ktor.http.ContentType 11 | import io.ktor.http.HttpStatusCode 12 | import io.ktor.server.application.call 13 | import io.ktor.server.auth.principal 14 | import io.ktor.server.plugins.BadRequestException 15 | import io.ktor.server.request.receive 16 | import io.ktor.server.response.respondText 17 | import io.ktor.server.routing.Route 18 | import io.ktor.server.routing.get 19 | import io.ktor.server.routing.post 20 | import org.koin.ktor.ext.inject 21 | 22 | fun Route.userAuthApi() { 23 | val userAuthService: UserAuthService by inject() 24 | val gson: Gson by inject() 25 | 26 | withPermission( 27 | "getUserAuth", 28 | getAuthorizationDetails(resourceNameIndex = 2, resourceInstanceIndex = 3, organizationIdIndex = 1), 29 | ) { 30 | get("/organizations/{organization_id}/users/{id}/auth_methods") { 31 | val organizationId = call.parameters["organization_id"]!! 32 | val userId = call.parameters["id"]!! 33 | val response = userAuthService.listUserAuth(organizationId, userId) 34 | call.respondText( 35 | text = gson.toJson(response), 36 | contentType = ContentType.Application.Json, 37 | status = HttpStatusCode.OK, 38 | ) 39 | } 40 | } 41 | 42 | withPermission( 43 | "addUserAuthMethod", 44 | getAuthorizationDetails(resourceNameIndex = 2, resourceInstanceIndex = 3, organizationIdIndex = 1), 45 | ) { 46 | post("/organizations/{organization_id}/users/{id}/auth_methods") { 47 | val organizationId = call.parameters["organization_id"]!! 48 | val userId = call.parameters["id"]!! 49 | val request = call.receive().validate() 50 | val token = request.token ?: throw BadRequestException("token is missing") 51 | val issuer = request.issuer ?: throw BadRequestException("issuer is missing") 52 | val principal = context.principal()!! 53 | val response = userAuthService.createUserAuth(organizationId, userId, issuer, token, principal) 54 | call.respondText( 55 | text = gson.toJson(response), 56 | contentType = ContentType.Application.Json, 57 | status = HttpStatusCode.Created, 58 | ) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/apis/ValidateApi.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.apis 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.models.ValidationRequest 5 | import com.hypto.iam.server.security.UserPrincipal 6 | import com.hypto.iam.server.service.ValidationService 7 | import com.hypto.iam.server.validators.validate 8 | import io.ktor.http.ContentType 9 | import io.ktor.http.HttpStatusCode 10 | import io.ktor.server.application.call 11 | import io.ktor.server.auth.principal 12 | import io.ktor.server.request.receive 13 | import io.ktor.server.response.respondText 14 | import io.ktor.server.routing.Route 15 | import io.ktor.server.routing.post 16 | import org.koin.ktor.ext.inject 17 | 18 | fun Route.validationApi() { 19 | val validationService: ValidationService by inject() 20 | val gson: Gson by inject() 21 | post("/validate") { 22 | val principal = context.principal()!! 23 | 24 | val request = call.receive().validate() 25 | 26 | val response = validationService.validateIfUserHasPermissionToActions(principal.hrn, request) 27 | 28 | call.respondText( 29 | text = gson.toJson(response), 30 | contentType = ContentType.Application.Json, 31 | status = HttpStatusCode.OK, 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/authProviders/AuthProviderRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.authProviders 2 | 3 | object AuthProviderRegistry { 4 | private var providerRegistry: Map = emptyMap() 5 | 6 | init { 7 | registerProvider(GoogleAuthProvider) 8 | registerProvider(MicrosoftAuthProvider) 9 | } 10 | 11 | fun getProvider(providerName: String) = providerRegistry[providerName] 12 | 13 | fun registerProvider(provider: BaseAuthProvider) { 14 | providerRegistry = providerRegistry.plus(provider.providerName to provider) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/authProviders/BaseAuthProvider.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.authProviders 2 | 3 | import com.hypto.iam.server.db.tables.records.UserAuthRecord 4 | import com.hypto.iam.server.security.OAuthUserPrincipal 5 | import com.hypto.iam.server.security.TokenCredential 6 | 7 | abstract class BaseAuthProvider(open val providerName: String, open val isVerifiedProvider: Boolean = true) { 8 | abstract fun getProfileDetails(tokenCredential: TokenCredential): OAuthUserPrincipal 9 | 10 | open suspend fun authenticate( 11 | principal: OAuthUserPrincipal, 12 | userAuthRecord: UserAuthRecord, 13 | ) { 14 | return 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/authProviders/GoogleAuthProvider.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.authProviders 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.ROOT_ORG 5 | import com.hypto.iam.server.exceptions.UnknownException 6 | import com.hypto.iam.server.logger 7 | import com.hypto.iam.server.security.AuthenticationException 8 | import com.hypto.iam.server.security.OAuthUserPrincipal 9 | import com.hypto.iam.server.security.TokenCredential 10 | import io.ktor.http.HttpStatusCode 11 | import okhttp3.OkHttpClient 12 | import okhttp3.Request 13 | import org.koin.core.component.KoinComponent 14 | import org.koin.core.component.inject 15 | import org.koin.core.qualifier.named 16 | 17 | object GoogleAuthProvider : BaseAuthProvider("google"), KoinComponent { 18 | private const val PROFILE_URL = "https://www.googleapis.com/oauth2/v3/userinfo" 19 | private const val ACCESS_TOKEN_KEY = "access_token" 20 | 21 | val gson: Gson by inject() 22 | private val httpClient: OkHttpClient by inject(named("AuthProvider")) 23 | 24 | override fun getProfileDetails(tokenCredential: TokenCredential): OAuthUserPrincipal { 25 | val requestBuilder = 26 | Request.Builder() 27 | .url("$PROFILE_URL?$ACCESS_TOKEN_KEY=${tokenCredential.value}") 28 | .method("GET", null) 29 | .addHeader("Content-Type", "application/json") 30 | val request = requestBuilder.build() 31 | val response = httpClient.newCall(request).execute() 32 | if (!response.isSuccessful) { 33 | logger.error("Status code: ${response.code} Body: ${response.body?.string()}") 34 | if (response.code == HttpStatusCode.Unauthorized.value) { 35 | throw AuthenticationException("Invalid access token") 36 | } 37 | throw UnknownException("Unable to fetch user details") 38 | } 39 | val googleUser = response.body?.string()?.let { this.gson.fromJson(it, GoogleUser::class.java) }!! 40 | return OAuthUserPrincipal( 41 | tokenCredential, 42 | ROOT_ORG, 43 | googleUser.email, 44 | googleUser.name, 45 | googleUser.hd ?: "", 46 | this.providerName, 47 | ) 48 | } 49 | } 50 | 51 | data class GoogleUser( 52 | val email: String, 53 | val name: String, 54 | val hd: String? = null, 55 | ) 56 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/authProviders/MicrosoftAuthProvider.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.authProviders 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.ROOT_ORG 5 | import com.hypto.iam.server.db.tables.records.UserAuthRecord 6 | import com.hypto.iam.server.exceptions.UnknownException 7 | import com.hypto.iam.server.logger 8 | import com.hypto.iam.server.security.AuthMetadata 9 | import com.hypto.iam.server.security.AuthenticationException 10 | import com.hypto.iam.server.security.OAuthUserPrincipal 11 | import com.hypto.iam.server.security.TokenCredential 12 | import io.ktor.http.HttpStatusCode 13 | import okhttp3.OkHttpClient 14 | import okhttp3.Request 15 | import org.koin.core.component.KoinComponent 16 | import org.koin.core.component.inject 17 | import org.koin.core.qualifier.named 18 | 19 | object MicrosoftAuthProvider : BaseAuthProvider("microsoft", false), KoinComponent { 20 | private const val PROFILE_URL = "https://graph.microsoft.com/v1.0/me" 21 | 22 | val gson: Gson by inject() 23 | private val httpClient: OkHttpClient by inject(named("AuthProvider")) 24 | 25 | override fun getProfileDetails(tokenCredential: TokenCredential): OAuthUserPrincipal { 26 | val requestBuilder = 27 | Request.Builder() 28 | .url(PROFILE_URL) 29 | .method("GET", null) 30 | .addHeader("Content-Type", "application/json") 31 | .addHeader("Authorization", "Bearer ${tokenCredential.value}") 32 | val request = requestBuilder.build() 33 | val response = httpClient.newCall(request).execute() 34 | if (!response.isSuccessful) { 35 | logger.error("Status code: ${response.code} Body: ${response.body?.string()}") 36 | if (response.code == HttpStatusCode.Unauthorized.value) { 37 | throw AuthenticationException("Invalid access token") 38 | } 39 | throw UnknownException("Unable to fetch user details") 40 | } 41 | val microsoftUser = response.body?.string()?.let { this.gson.fromJson(it, MicrosoftUser::class.java) }!! 42 | if (microsoftUser.mail == null) { 43 | throw AuthenticationException("Email not associated with Microsoft profile") 44 | } 45 | return OAuthUserPrincipal( 46 | tokenCredential, 47 | ROOT_ORG, 48 | microsoftUser.mail, 49 | microsoftUser.displayName, 50 | "", 51 | this.providerName, 52 | AuthMetadata( 53 | id = microsoftUser.id, 54 | ), 55 | ) 56 | } 57 | 58 | override suspend fun authenticate( 59 | principal: OAuthUserPrincipal, 60 | userAuthRecord: UserAuthRecord, 61 | ) { 62 | val principalMetadata = principal.metadata 63 | val authMetadata = AuthMetadata.from(userAuthRecord.authMetadata) 64 | 65 | // Reason to use id instead of email is because email can be changed in Microsoft profile 66 | // Blog: https://0x8.in/blog/2021/04/30/mip-oid-sub/ 67 | if (principalMetadata?.id == null || authMetadata.id == null || authMetadata.id != principalMetadata.id) { 68 | throw AuthenticationException("User is not authenticated with Microsoft") 69 | } 70 | 71 | return 72 | } 73 | } 74 | 75 | data class MicrosoftUser( 76 | val id: String, 77 | val mail: String? = null, 78 | val displayName: String, 79 | ) 80 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/listeners/DeleteOrUpdateWithoutWhereListener.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.listeners 2 | 3 | import org.jooq.ExecuteContext 4 | import org.jooq.impl.DefaultExecuteListener 5 | 6 | // Repurposed from: https://www.jooq.org/doc/latest/manual/sql-execution/execute-listeners/#NA7281 7 | 8 | /** 9 | * Throws DeleteOrUpdateWithoutWhereException if UPDATE or DELETE statements does not contain a WHERE clause. 10 | * 11 | * @see DeleteOrUpdateWithoutWhereException 12 | */ 13 | class DeleteOrUpdateWithoutWhereListener : DefaultExecuteListener() { 14 | override fun renderEnd(ctx: ExecuteContext) { 15 | if (ctx.sql()!!.matches(Regex("^(?i:(UPDATE|DELETE)(?!.* WHERE ).*)$"))) { 16 | throw DeleteOrUpdateWithoutWhereException() 17 | } 18 | } 19 | } 20 | 21 | class DeleteOrUpdateWithoutWhereException : RuntimeException() 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/ActionRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.hypto.iam.server.db.Tables.ACTIONS 4 | import com.hypto.iam.server.db.tables.pojos.Actions 5 | import com.hypto.iam.server.db.tables.records.ActionsRecord 6 | import com.hypto.iam.server.extensions.PaginationContext 7 | import com.hypto.iam.server.extensions.paginate 8 | import com.hypto.iam.server.utils.ActionHrn 9 | import com.hypto.iam.server.utils.ResourceHrn 10 | import org.jooq.Result 11 | import org.jooq.impl.DAOImpl 12 | import java.time.LocalDateTime 13 | 14 | object ActionRepo : BaseRepo() { 15 | private val idFun = fun (action: Actions): String { 16 | return action.hrn 17 | } 18 | 19 | override suspend fun dao(): DAOImpl { 20 | return txMan.getDao(com.hypto.iam.server.db.tables.Actions.ACTIONS, Actions::class.java, idFun) 21 | } 22 | 23 | suspend fun fetchByHrn(hrn: ActionHrn): ActionsRecord? { 24 | return ctx("action.findByHrn").selectFrom(ACTIONS).where(ACTIONS.HRN.eq(hrn.toString())).fetchOne() 25 | } 26 | 27 | suspend fun create( 28 | orgId: String, 29 | resourceHrn: ResourceHrn, 30 | hrn: ActionHrn, 31 | description: String?, 32 | ): ActionsRecord { 33 | val record = 34 | ActionsRecord() 35 | .setHrn(hrn.toString()) 36 | .setOrganizationId(orgId) 37 | .setResourceHrn(resourceHrn.toString()) 38 | .setDescription(description) 39 | .setCreatedAt(LocalDateTime.now()) 40 | .setUpdatedAt(LocalDateTime.now()) 41 | 42 | record.attach(dao().configuration()) 43 | record.store() 44 | return record 45 | } 46 | 47 | suspend fun update( 48 | hrn: ActionHrn, 49 | description: String, 50 | ): ActionsRecord? { 51 | val condition = ACTIONS.HRN.eq(hrn.toString()) 52 | return ctx("action.update").update(ACTIONS) 53 | .set(ACTIONS.DESCRIPTION, description) 54 | .set(ACTIONS.UPDATED_AT, LocalDateTime.now()) 55 | .where(condition) 56 | .returning() 57 | .fetchOne() 58 | } 59 | 60 | suspend fun fetchActionsPaginated( 61 | organizationId: String, 62 | resourceHrn: ResourceHrn, 63 | paginationContext: PaginationContext, 64 | ): Result { 65 | return ctx("action.fetchPaginated").selectFrom(ACTIONS) 66 | .where(ACTIONS.ORGANIZATION_ID.eq(organizationId).and(ACTIONS.RESOURCE_HRN.eq(resourceHrn.toString()))) 67 | .paginate(ACTIONS.HRN, paginationContext) 68 | .fetch() 69 | } 70 | 71 | suspend fun delete(hrn: ActionHrn): Boolean { 72 | val record = ActionsRecord().setHrn(hrn.toString()) 73 | record.attach(dao().configuration()) 74 | return record.delete() > 0 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/AuditEntriesRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.hypto.iam.server.db.tables.AuditEntries.AUDIT_ENTRIES 4 | import com.hypto.iam.server.db.tables.pojos.AuditEntries 5 | import com.hypto.iam.server.db.tables.records.AuditEntriesRecord 6 | import com.hypto.iam.server.utils.Hrn 7 | import com.hypto.iam.server.utils.HrnFactory 8 | import com.hypto.iam.server.utils.ResourceHrn 9 | import mu.KotlinLogging 10 | import org.jooq.Result 11 | import org.jooq.impl.DAOImpl 12 | import org.jooq.impl.DSL 13 | import org.koin.core.component.inject 14 | import java.time.LocalDateTime 15 | import java.util.UUID 16 | 17 | object AuditEntriesRepo : BaseRepo() { 18 | private val logger = KotlinLogging.logger { } 19 | private val hrnFactory by inject() 20 | 21 | private val idFun = fun (auditEntry: AuditEntries): UUID { 22 | return auditEntry.id 23 | } 24 | 25 | override suspend fun dao(): DAOImpl { 26 | return txMan.getDao(AUDIT_ENTRIES, AuditEntries::class.java, idFun) 27 | } 28 | 29 | suspend fun fetchByPrincipalAndTime( 30 | principalOrg: String, 31 | principal: Hrn, 32 | eventTimeStart: LocalDateTime, 33 | eventTimeEnd: LocalDateTime, 34 | ): Result { 35 | return ctx() 36 | .selectFrom(AUDIT_ENTRIES) 37 | .where(AUDIT_ENTRIES.PRINCIPAL_ORGANIZATION.eq(principalOrg)) 38 | .and(AUDIT_ENTRIES.PRINCIPAL.eq(principal.toString())) 39 | .and(AUDIT_ENTRIES.EVENT_TIME.ge(eventTimeStart)) 40 | .and(AUDIT_ENTRIES.EVENT_TIME.ge(eventTimeEnd)) 41 | .fetch() 42 | } 43 | 44 | suspend fun fetchByResourceAndTime( 45 | resource: Hrn, 46 | eventTimeStart: LocalDateTime, 47 | eventTimeEnd: LocalDateTime, 48 | operations: List?, 49 | ): Result { 50 | var query = 51 | ctx() 52 | .selectFrom(AUDIT_ENTRIES) 53 | .where(AUDIT_ENTRIES.RESOURCE.eq(resource.toString())) 54 | .and(AUDIT_ENTRIES.EVENT_TIME.ge(eventTimeStart)) 55 | .and(AUDIT_ENTRIES.EVENT_TIME.ge(eventTimeEnd)) 56 | if (operations != null) { 57 | query = query.and(AUDIT_ENTRIES.OPERATION.`in`(operations.map { it.toString() })) 58 | } 59 | 60 | return query.fetch() 61 | } 62 | 63 | suspend fun batchInsert(auditEntries: List): Boolean { 64 | val batchBindStep = 65 | ctx().batch( 66 | DSL.insertInto( 67 | AUDIT_ENTRIES, 68 | AUDIT_ENTRIES.REQUEST_ID, 69 | AUDIT_ENTRIES.EVENT_TIME, 70 | AUDIT_ENTRIES.PRINCIPAL, 71 | AUDIT_ENTRIES.PRINCIPAL_ORGANIZATION, 72 | AUDIT_ENTRIES.RESOURCE, 73 | AUDIT_ENTRIES.OPERATION, 74 | // No meta arguments at the moment 75 | ).values(null as String?, null, null, null, null, null), 76 | ) 77 | auditEntries.forEach { 78 | val principalHrn: ResourceHrn = hrnFactory.getHrn(it.principal) as ResourceHrn 79 | batchBindStep.bind( 80 | it.requestId, 81 | it.eventTime, 82 | it.principal, 83 | principalHrn.organization, 84 | it.resource, 85 | it.operation, 86 | ) 87 | } 88 | val result = batchBindStep.execute() 89 | var returnValue = true 90 | 91 | result.forEachIndexed { index, insertCount -> 92 | if (insertCount != 1) { 93 | returnValue = false 94 | logger.error { "Insert failed: ${auditEntries[index]}" } 95 | } 96 | } 97 | return returnValue 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/AuthProviderRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.hypto.iam.server.db.Tables.AUTH_PROVIDER 4 | import com.hypto.iam.server.db.tables.pojos.AuthProvider 5 | import com.hypto.iam.server.db.tables.records.AuthProviderRecord 6 | import com.hypto.iam.server.extensions.PaginationContext 7 | import com.hypto.iam.server.extensions.paginate 8 | import org.jooq.DSLContext 9 | import org.jooq.Record1 10 | import org.jooq.impl.DAOImpl 11 | 12 | typealias AuthProviderPk = Record1 13 | 14 | object AuthProviderRepo : BaseRepo() { 15 | @Suppress("ktlint:standard:blank-line-before-declaration") 16 | private fun getIdFun(dsl: DSLContext): (AuthProvider) -> AuthProviderPk { 17 | return fun (authProvider: AuthProvider): AuthProviderPk { 18 | return dsl.newRecord( 19 | AUTH_PROVIDER.PROVIDER_NAME, 20 | ) 21 | .values(authProvider.providerName) 22 | } 23 | } 24 | 25 | override suspend fun dao(): DAOImpl { 26 | return txMan.getDao( 27 | AUTH_PROVIDER, 28 | AuthProvider::class.java, 29 | getIdFun(txMan.dsl()), 30 | ) 31 | } 32 | 33 | suspend fun fetchAuthProvidersPaginated( 34 | paginationContext: PaginationContext, 35 | ): List { 36 | return ctx("authProvider.fetchAuthProvidersPaginated").selectFrom(AUTH_PROVIDER) 37 | .where() 38 | .paginate(AUTH_PROVIDER.PROVIDER_NAME, paginationContext) 39 | .fetch() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/BaseRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.hypto.iam.server.Constants.Companion.JOOQ_QUERY_NAME 4 | import com.txman.TxMan 5 | import io.micrometer.core.instrument.MeterRegistry 6 | import io.micrometer.core.instrument.binder.db.MetricsDSLContext 7 | import org.jooq.Condition 8 | import org.jooq.DSLContext 9 | import org.jooq.Field 10 | import org.jooq.Record 11 | import org.jooq.TableField 12 | import org.jooq.UpdatableRecord 13 | import org.jooq.impl.DAOImpl 14 | import org.jooq.impl.DSL 15 | import org.koin.core.component.KoinComponent 16 | import org.koin.core.component.inject 17 | 18 | abstract class BaseRepo, P, T> : KoinComponent { 19 | protected val txMan: TxMan by inject() 20 | private val micrometerRegistry: MeterRegistry by inject() 21 | 22 | abstract suspend fun dao(): DAOImpl 23 | 24 | suspend fun ctx(name: String? = null): DSLContext = 25 | MetricsDSLContext.withMetrics(dao().ctx(), micrometerRegistry, emptyList()) 26 | .apply { name?.let { this.tag(JOOQ_QUERY_NAME, name) } } 27 | 28 | suspend fun delete(pojo: P) = dao().delete(pojo) 29 | 30 | suspend fun fetch( 31 | field: Field, 32 | vararg values: Z, 33 | ): List

= dao().fetch(field, *values) 34 | 35 | suspend fun fetchOne( 36 | field: Field, 37 | value: Z, 38 | ): P? = dao().fetchOne(field, value) 39 | 40 | suspend fun existsById(id: T): Boolean = dao().existsById(id) 41 | 42 | suspend fun insert(pojo: P) = dao().insert(pojo) 43 | 44 | suspend fun store(record: R): Int = record.apply { attach(dao().configuration()) }.store() 45 | 46 | suspend fun delete(record: R): Int = record.apply { attach(dao().configuration()) }.delete() 47 | 48 | suspend fun deleteBatch(keys: Collection): Int { 49 | val pk = pk() 50 | requireNotNull(pk) { "Cannot batchDelete on ${dao().table.name} as primaryKey is unavailable" } 51 | return ctx().deleteFrom(dao().table).where(equal(pk, keys)).execute() 52 | } 53 | 54 | suspend fun insertBatch( 55 | queryName: String? = null, 56 | records: List, 57 | ): List { 58 | if (records.isEmpty()) return records 59 | var insertStep = ctx(queryName).insertInto(dao().table).set(records[0]) 60 | for (i in 1 until records.size) { 61 | insertStep = insertStep.newRecord().set(records[i]) 62 | } 63 | return insertStep.returning().fetch() 64 | } 65 | 66 | suspend fun batchStore( 67 | queryName: String? = null, 68 | records: List, 69 | ) = ctx(queryName).batchStore(records).execute() 70 | 71 | // ------------------------------------------------------------------------ 72 | // XXX: Private utility methods: Repurposed from JOOQ's DAOImpl.java 73 | // ------------------------------------------------------------------------ 74 | 75 | private fun equal( 76 | pk: Array>, 77 | id: T, 78 | ): Condition { 79 | @Suppress("SpreadOperator") 80 | return if (pk.size == 1) { 81 | (pk[0] as Field).equal(pk[0].dataType.convert(id)) 82 | } else { 83 | DSL.row(*pk).equal(id as Record) 84 | } 85 | } 86 | 87 | private fun equal( 88 | pk: Array>, 89 | ids: Collection, 90 | ): Condition { 91 | return if (pk.size == 1) { 92 | if (ids.size == 1) { 93 | equal(pk, ids.iterator().next()) 94 | } else { 95 | (pk[0]).`in`(pk[0].dataType.convert(ids)) 96 | } 97 | } else { 98 | // [#2573] Composite key T types are of type Record[N] 99 | TODO("Composite keys are unsupported at the moment") 100 | // DSL.row(*pk).`in`(ids as MutableCollection) 101 | } 102 | } 103 | 104 | private suspend fun pk(): Array>? { 105 | return dao().table.primaryKey?.fieldsArray 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/LinkUsersRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.hypto.iam.server.db.tables.LinkUsers.LINK_USERS 4 | import com.hypto.iam.server.db.tables.pojos.LinkUsers 5 | import com.hypto.iam.server.db.tables.records.LinkUsersRecord 6 | import com.hypto.iam.server.extensions.PaginationContext 7 | import com.hypto.iam.server.extensions.paginate 8 | import org.jooq.impl.DAOImpl 9 | 10 | object LinkUsersRepo : BaseRepo() { 11 | private val idFun = fun(linkUsers: LinkUsers) = linkUsers.id 12 | 13 | override suspend fun dao(): DAOImpl = 14 | txMan.getDao(LINK_USERS, LinkUsers::class.java, idFun) 15 | 16 | suspend fun getById(id: String) = 17 | ctx("linkUsers.getById").selectFrom(LINK_USERS) 18 | .where(LINK_USERS.ID.eq(id)) 19 | .fetchOne() 20 | 21 | suspend fun fetchSubordinateUsers( 22 | leaderUserHrn: String, 23 | context: PaginationContext, 24 | ): Map { 25 | return ctx("linkUsers.fetchSubordinateUsers") 26 | .selectFrom(LINK_USERS) 27 | .where(LINK_USERS.LEADER_USER_HRN.eq(leaderUserHrn)) 28 | .paginate(LINK_USERS.SUBORDINATE_USER_HRN, context) 29 | .fetchMap(LINK_USERS.SUBORDINATE_USER_HRN) 30 | } 31 | 32 | suspend fun fetchLeaderUsers( 33 | subordinateUserHrn: String, 34 | context: PaginationContext, 35 | ): Map { 36 | return ctx("linkUsers.fetchLeaderUsers") 37 | .selectFrom(LINK_USERS) 38 | .where(LINK_USERS.SUBORDINATE_USER_HRN.eq(subordinateUserHrn)) 39 | .paginate(LINK_USERS.LEADER_USER_HRN, context) 40 | .fetchMap(LINK_USERS.LEADER_USER_HRN) 41 | } 42 | 43 | suspend fun deleteById(id: String): Boolean { 44 | return ctx("linkUsers.deleteById").deleteFrom(LINK_USERS) 45 | .where(LINK_USERS.ID.eq(id)) 46 | .execute() > 0 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/OrganizationRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.db.tables.Organizations.ORGANIZATIONS 5 | import com.hypto.iam.server.db.tables.pojos.Organizations 6 | import com.hypto.iam.server.db.tables.records.OrganizationsRecord 7 | import com.hypto.iam.server.idp.IdentityGroup 8 | import org.jooq.JSONB 9 | import org.jooq.impl.DAOImpl 10 | import org.koin.core.component.inject 11 | import java.time.LocalDateTime 12 | 13 | object OrganizationRepo : BaseRepo() { 14 | private val gson: Gson by inject() 15 | 16 | private val idFun = fun (organization: Organizations): String { 17 | return organization.id 18 | } 19 | 20 | override suspend fun dao(): DAOImpl = 21 | txMan.getDao(ORGANIZATIONS, Organizations::class.java, idFun) 22 | 23 | /** 24 | * Updates organization with given input values 25 | */ 26 | suspend fun update( 27 | id: String, 28 | name: String?, 29 | description: String?, 30 | identityGroup: IdentityGroup?, 31 | ): OrganizationsRecord? { 32 | val updateStep = 33 | ctx("organizations.update") 34 | .update(ORGANIZATIONS) 35 | .set(ORGANIZATIONS.UPDATED_AT, LocalDateTime.now()) 36 | if (!name.isNullOrEmpty()) { 37 | updateStep.set(ORGANIZATIONS.NAME, name) 38 | } 39 | if (!description.isNullOrEmpty()) { 40 | updateStep.set(ORGANIZATIONS.DESCRIPTION, description) 41 | } 42 | if (identityGroup != null) { 43 | val identityGroup = gson.toJson(identityGroup, IdentityGroup::class.java) 44 | updateStep.set(ORGANIZATIONS.METADATA, JSONB.jsonb(identityGroup)) 45 | } 46 | return updateStep.where(ORGANIZATIONS.ID.eq(id)).returning().fetchOne() 47 | } 48 | 49 | suspend fun findById(id: String): Organizations? = dao().findById(id) 50 | 51 | suspend fun deleteById(id: String) = dao().deleteById(id) 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/PolicyTemplatesRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.hypto.iam.server.db.Tables.POLICY_TEMPLATES 4 | import com.hypto.iam.server.db.tables.pojos.PolicyTemplates 5 | import com.hypto.iam.server.db.tables.records.PolicyTemplatesRecord 6 | import org.jooq.Result 7 | import org.jooq.impl.DAOImpl 8 | 9 | object PolicyTemplatesRepo : BaseRepo() { 10 | enum class Status(val value: String) { 11 | ACTIVE("ACTIVE"), 12 | ARCHIVED("ARCHIVED"), 13 | } 14 | 15 | private val idFun = fun (policyTemplates: PolicyTemplates) = policyTemplates.name 16 | 17 | override suspend fun dao(): DAOImpl = 18 | txMan.getDao(POLICY_TEMPLATES, PolicyTemplates::class.java, idFun) 19 | 20 | /** 21 | * Fetch records that have `status = 'ACTIVE'` 22 | */ 23 | suspend fun fetchActivePolicyTemplatesForOrgCreation(): Result = 24 | ctx("policy_templates.fetchActive") 25 | .selectFrom(POLICY_TEMPLATES) 26 | .where(POLICY_TEMPLATES.STATUS.eq(Status.ACTIVE.value)) 27 | .and(POLICY_TEMPLATES.ON_CREATE_ORG.eq(true)) 28 | .fetch() 29 | 30 | suspend fun fetchActivePolicyByName(name: String): PolicyTemplatesRecord? = 31 | ctx("policy_templates.fetchActivePolicyByName") 32 | .selectFrom(POLICY_TEMPLATES) 33 | .where( 34 | POLICY_TEMPLATES.NAME.eq(name), 35 | POLICY_TEMPLATES.STATUS.eq(Status.ACTIVE.value), 36 | ) 37 | .fetchOne() 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/PrincipalPoliciesRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.hypto.iam.server.db.Tables.PRINCIPAL_POLICIES 4 | import com.hypto.iam.server.db.tables.Policies.POLICIES 5 | import com.hypto.iam.server.db.tables.pojos.PrincipalPolicies 6 | import com.hypto.iam.server.db.tables.records.PoliciesRecord 7 | import com.hypto.iam.server.db.tables.records.PrincipalPoliciesRecord 8 | import com.hypto.iam.server.extensions.PaginationContext 9 | import com.hypto.iam.server.extensions.customPaginate 10 | import com.hypto.iam.server.utils.Hrn 11 | import org.jooq.Record 12 | import org.jooq.Result 13 | import org.jooq.TableField 14 | import org.jooq.impl.DAOImpl 15 | import java.util.UUID 16 | 17 | object PrincipalPoliciesRepo : BaseRepo() { 18 | private val idFun = fun (principalPolicies: PrincipalPolicies): UUID = principalPolicies.id 19 | 20 | override suspend fun dao(): DAOImpl = 21 | txMan.getDao(PRINCIPAL_POLICIES, PrincipalPolicies::class.java, idFun) 22 | 23 | /** 24 | * Fetch records that have `principal_hrn = value` 25 | */ 26 | suspend fun fetchByPrincipalHrn(value: String): Result = 27 | ctx("principalPolicies.fetchByPrincipalHrn") 28 | .selectFrom(PRINCIPAL_POLICIES) 29 | .where(PRINCIPAL_POLICIES.PRINCIPAL_HRN.equal(value)) 30 | .fetch() 31 | 32 | suspend fun fetchPoliciesByUserHrnPaginated( 33 | userHrn: String, 34 | paginationContext: PaginationContext, 35 | ): Result { 36 | return customPaginate( 37 | ctx("principalPolicies.fetchByPrincipalHrnPaginated") 38 | .select(POLICIES.fields().asList()) 39 | .from( 40 | PRINCIPAL_POLICIES.join(POLICIES).on( 41 | com.hypto.iam.server.db.Tables.POLICIES.HRN.eq(PRINCIPAL_POLICIES.POLICY_HRN), 42 | ), 43 | ) 44 | .where(PRINCIPAL_POLICIES.PRINCIPAL_HRN.eq(userHrn)), 45 | PRINCIPAL_POLICIES.POLICY_HRN as TableField, 46 | paginationContext, 47 | ).fetchInto(POLICIES) 48 | } 49 | 50 | suspend fun insert(records: List): Result { 51 | // No way to return generated values 52 | // https://github.com/jooq/jooq/issues/3327 53 | // return ctx().batchInsert(records).execute() 54 | 55 | var insertStep = ctx("principalPolicies.insertBatch").insertInto(PRINCIPAL_POLICIES) 56 | for (i in 0 until records.size - 1) { 57 | insertStep = insertStep.set(records[i]).newRecord() 58 | } 59 | val lastRecord = records[records.size - 1] 60 | 61 | return insertStep.set(lastRecord).returning().fetch() 62 | } 63 | 64 | suspend fun delete( 65 | userHrn: Hrn, 66 | policies: List, 67 | ): Boolean = 68 | ctx("principalPolicies.delete") 69 | .deleteFrom(PRINCIPAL_POLICIES) 70 | .where( 71 | PRINCIPAL_POLICIES.PRINCIPAL_HRN.eq(userHrn.toString()), 72 | PRINCIPAL_POLICIES.POLICY_HRN.`in`(policies), 73 | ) 74 | .execute() > 0 75 | 76 | suspend fun deleteByPrincipalHrn(principalHrn: String): Boolean = 77 | ctx("principalPolicies.deleteByPrincipalHrn") 78 | .deleteFrom(PRINCIPAL_POLICIES) 79 | .where(PRINCIPAL_POLICIES.PRINCIPAL_HRN.like("%$principalHrn%")) 80 | .execute() > 0 81 | 82 | suspend fun deleteByPolicyHrn(policyHrn: String): Boolean = 83 | ctx("principalPolicies.deleteByPrincipalHrn") 84 | .deleteFrom(PRINCIPAL_POLICIES) 85 | .where(PRINCIPAL_POLICIES.POLICY_HRN.eq(policyHrn)) 86 | .execute() > 0 87 | } 88 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/ResourceRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.hypto.iam.server.db.Tables.RESOURCES 4 | import com.hypto.iam.server.db.tables.pojos.Resources 5 | import com.hypto.iam.server.db.tables.records.ResourcesRecord 6 | import com.hypto.iam.server.extensions.PaginationContext 7 | import com.hypto.iam.server.extensions.paginate 8 | import com.hypto.iam.server.utils.ResourceHrn 9 | import org.jooq.Result 10 | import org.jooq.impl.DAOImpl 11 | import java.time.LocalDateTime 12 | 13 | object ResourceRepo : BaseRepo() { 14 | private val idFun = fun (resource: Resources) = resource.hrn 15 | 16 | override suspend fun dao(): DAOImpl = 17 | txMan.getDao(RESOURCES, Resources::class.java, idFun) 18 | 19 | /** 20 | * Fetch a unique record that has `hrn = value` 21 | */ 22 | suspend fun fetchByHrn(hrn: ResourceHrn): ResourcesRecord? = 23 | ctx("resources.fetchByHrn") 24 | .selectFrom(RESOURCES).where(RESOURCES.HRN.equal(hrn.toString())).fetchOne() 25 | 26 | suspend fun create( 27 | hrn: ResourceHrn, 28 | description: String, 29 | ): ResourcesRecord { 30 | val record = 31 | ResourcesRecord() 32 | .setHrn(hrn.toString()) 33 | .setOrganizationId(hrn.organization) 34 | .setCreatedAt(LocalDateTime.now()) 35 | .setUpdatedAt(LocalDateTime.now()) 36 | .setDescription(description) 37 | 38 | record.attach(dao().configuration()) 39 | record.store() 40 | return record 41 | } 42 | 43 | suspend fun update( 44 | hrn: ResourceHrn, 45 | description: String, 46 | ): ResourcesRecord? = 47 | ctx("resources.update") 48 | .update(RESOURCES) 49 | .set(RESOURCES.DESCRIPTION, description) 50 | .set(RESOURCES.UPDATED_AT, LocalDateTime.now()) 51 | .where(RESOURCES.HRN.equal(hrn.toString())) 52 | .returning() 53 | .fetchOne() 54 | 55 | suspend fun delete(hrn: ResourceHrn): Boolean { 56 | val record = ResourcesRecord().setHrn(hrn.toString()) 57 | record.attach(dao().configuration()) 58 | return record.delete() > 0 59 | } 60 | 61 | suspend fun fetchByOrganizationIdPaginated( 62 | organizationId: String, 63 | paginationContext: PaginationContext, 64 | ): Result = 65 | ctx("resources.fetchPaginated") 66 | .selectFrom(RESOURCES) 67 | .where(RESOURCES.ORGANIZATION_ID.eq(organizationId)) 68 | .paginate(RESOURCES.HRN, paginationContext) 69 | .fetch() 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/UserAuthProvidersRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.hypto.iam.server.db.tables.pojos.UsersAuthProviders 4 | import com.hypto.iam.server.db.tables.records.UsersAuthProvidersRecord 5 | import org.jooq.impl.DAOImpl 6 | 7 | object UserAuthProvidersRepo : BaseRepo() { 8 | private val idFun = fun (usersAuthProviders: UsersAuthProviders) = usersAuthProviders.id 9 | 10 | override suspend fun dao(): DAOImpl = 11 | txMan.getDao( 12 | com.hypto.iam.server.db.tables.UsersAuthProviders.USERS_AUTH_PROVIDERS, 13 | UsersAuthProviders::class.java, 14 | idFun, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/db/repositories/UserAuthRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.db.repositories 2 | 3 | import com.hypto.iam.server.db.Tables.USER_AUTH 4 | import com.hypto.iam.server.db.tables.pojos.UserAuth 5 | import com.hypto.iam.server.db.tables.records.UserAuthRecord 6 | import com.hypto.iam.server.utils.Hrn 7 | import org.jooq.DSLContext 8 | import org.jooq.JSONB 9 | import org.jooq.Record2 10 | import org.jooq.impl.DAOImpl 11 | import java.time.LocalDateTime 12 | 13 | typealias UserAuthPk = Record2 14 | 15 | object UserAuthRepo : BaseRepo() { 16 | @Suppress("ktlint:standard:blank-line-before-declaration") 17 | private fun getIdFun(dsl: DSLContext): (UserAuth) -> UserAuthPk { 18 | return fun (userAuth: UserAuth): UserAuthPk { 19 | return dsl.newRecord( 20 | USER_AUTH.USER_HRN, 21 | USER_AUTH.PROVIDER_NAME, 22 | ) 23 | .values(userAuth.userHrn, userAuth.providerName) 24 | } 25 | } 26 | 27 | override suspend fun dao(): DAOImpl { 28 | return txMan.getDao( 29 | USER_AUTH, 30 | UserAuth::class.java, 31 | getIdFun(txMan.dsl()), 32 | ) 33 | } 34 | 35 | suspend fun fetchByUserHrnAndProviderName( 36 | hrn: String, 37 | providerName: String, 38 | ): UserAuthRecord? { 39 | return UserAuthRepo 40 | .ctx("userAuth.findByUserHrnAndProviderName") 41 | .selectFrom(USER_AUTH) 42 | .where(USER_AUTH.USER_HRN.eq(hrn).and(USER_AUTH.PROVIDER_NAME.eq(providerName))) 43 | .fetchOne() 44 | } 45 | 46 | suspend fun create( 47 | hrn: String, 48 | providerName: String, 49 | authMetadata: JSONB?, 50 | ): UserAuthRecord { 51 | val logTimestamp = LocalDateTime.now() 52 | val record = 53 | UserAuthRecord() 54 | .setUserHrn(hrn) 55 | .setProviderName(providerName) 56 | .setAuthMetadata(authMetadata) 57 | .setCreatedAt(logTimestamp) 58 | .setUpdatedAt(logTimestamp) 59 | 60 | record.attach(dao().configuration()) 61 | record.store() 62 | return record 63 | } 64 | 65 | suspend fun fetchUserAuth( 66 | userHrn: Hrn, 67 | ): List { 68 | return ctx("userAuth.fetchUserAuth").selectFrom(USER_AUTH) 69 | .where(USER_AUTH.USER_HRN.eq(userHrn.toString())) 70 | .fetch() 71 | } 72 | 73 | suspend fun deleteByUserHrn(userHrn: String): Boolean { 74 | val count = 75 | ctx("userAuth.deleteByUserHrn") 76 | .deleteFrom(USER_AUTH) 77 | .where(USER_AUTH.USER_HRN.eq(userHrn)) 78 | .execute() 79 | return count > 0 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/exceptions/ApplicationExceptions.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.exceptions 2 | 3 | class InternalException(s: String, ex: Exception? = null) : Exception(s, ex) 4 | 5 | class UnknownException(s: String, ex: Exception? = null) : Exception(s, ex) 6 | 7 | class EntityAlreadyExistsException(s: String, ex: Exception? = null) : Exception(s, ex) 8 | 9 | class EntityNotFoundException(s: String, ex: Exception? = null) : Exception(s, ex) 10 | 11 | class JwtExpiredException(s: String, ex: Exception? = null) : Exception(s, ex) 12 | 13 | class InvalidJwtException(s: String, ex: Exception? = null) : Exception(s, ex) 14 | 15 | class PolicyFormatException(s: String, ex: Exception? = null) : Exception(s, ex) 16 | 17 | class PublicKeyExpiredException(s: String, ex: Exception? = null) : Exception(s, ex) 18 | 19 | class PasscodeExpiredException(s: String, ex: Exception? = null) : Exception(s, ex) 20 | 21 | class PasscodeLimitExceededException(s: String, ex: Exception? = null) : Exception(s, ex) 22 | 23 | class ActionNotPermittedException(s: String, ex: Exception? = null) : Exception(s, ex) 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/exceptions/DbExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.exceptions 2 | 3 | import io.ktor.server.plugins.BadRequestException 4 | import org.jooq.exception.DataAccessException 5 | import kotlin.reflect.KClass 6 | import kotlin.reflect.full.primaryConstructor 7 | 8 | object DbExceptionHandler { 9 | data class DbExceptionMap( 10 | val errorMessage: String, 11 | val constraintRegex: Regex, 12 | val appExceptions: Map, String>>, 13 | ) 14 | 15 | private val customExceptions: Set> = 16 | setOf( 17 | BadRequestException::class, 18 | EntityNotFoundException::class, 19 | EntityAlreadyExistsException::class, 20 | ) 21 | private val duplicateConstraintRegex = "\"(.+)?\"".toRegex() 22 | private val foreignKeyConstraintRegex = "foreign key constraint \"(.+)?\"".toRegex() 23 | 24 | private val duplicateExceptions = mapOf, String>>() 25 | 26 | private val foreignKeyExceptions = mapOf, String>>() 27 | 28 | private val dbExceptionMap = 29 | listOf( 30 | DbExceptionMap( 31 | "duplicate key value violates unique constraint", 32 | duplicateConstraintRegex, 33 | duplicateExceptions, 34 | ), 35 | DbExceptionMap( 36 | "violates foreign key constraint", 37 | foreignKeyConstraintRegex, 38 | foreignKeyExceptions, 39 | ), 40 | ) 41 | 42 | fun mapToApplicationException(e: DataAccessException): Exception { 43 | var finalException: Exception? = null 44 | val causeMessage = e.cause?.message ?: e.message 45 | 46 | e.cause?.let { 47 | if (customExceptions.contains(it::class)) { 48 | return it as Exception 49 | } 50 | } 51 | 52 | dbExceptionMap.forEach { 53 | if (causeMessage?.contains(it.errorMessage) == true) { 54 | val constraintKey = it.constraintRegex.find(causeMessage)?.groups?.get(1)?.value 55 | val exceptionPair = it.appExceptions[constraintKey] 56 | if (exceptionPair != null) { 57 | finalException = exceptionPair.first.primaryConstructor?.call(exceptionPair.second, e) 58 | } 59 | } 60 | } 61 | 62 | return finalException ?: InternalException("Unknown error occurred", e) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/extensions/Routing.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.extensions 2 | 3 | import com.hypto.iam.server.security.getAuthorizationDetails 4 | import com.hypto.iam.server.security.withPermission 5 | import io.ktor.server.application.ApplicationCall 6 | import io.ktor.server.routing.Route 7 | import io.ktor.server.routing.delete 8 | import io.ktor.server.routing.get 9 | import io.ktor.server.routing.patch 10 | import io.ktor.server.routing.post 11 | import io.ktor.util.pipeline.PipelineContext 12 | 13 | // Extension functions to support multiple paths for a single body 14 | fun Route.get( 15 | vararg paths: String, 16 | body: suspend PipelineContext.(Unit) -> Unit, 17 | ) { 18 | for (path in paths) { 19 | get(path, body) 20 | } 21 | } 22 | 23 | fun Route.patch( 24 | vararg paths: String, 25 | body: suspend PipelineContext.(Unit) -> Unit, 26 | ) { 27 | for (path in paths) { 28 | patch(path, body) 29 | } 30 | } 31 | 32 | fun Route.post( 33 | vararg paths: String, 34 | body: suspend PipelineContext.(Unit) -> Unit, 35 | ) { 36 | for (path in paths) { 37 | post(path, body) 38 | } 39 | } 40 | 41 | fun Route.delete( 42 | vararg paths: String, 43 | body: suspend PipelineContext.(Unit) -> Unit, 44 | ) { 45 | for (path in paths) { 46 | delete(path, body) 47 | } 48 | } 49 | 50 | // Functions composing withPermission and routing builder functions like get, post, etc. 51 | fun Route.getWithPermission( 52 | routeOptions: List, 53 | action: String, 54 | validateOrgIdFromPath: Boolean = true, 55 | body: suspend PipelineContext.(Unit) -> Unit, 56 | ) { 57 | for (routeOption in routeOptions) { 58 | withPermission( 59 | action, 60 | getAuthorizationDetails(routeOption), 61 | validateOrgIdFromPath, 62 | ) { 63 | get(routeOption.pathTemplate, body) 64 | } 65 | } 66 | } 67 | 68 | fun Route.patchWithPermission( 69 | routeOptions: List, 70 | action: String, 71 | validateOrgIdFromPath: Boolean = true, 72 | body: suspend PipelineContext.(Unit) -> Unit, 73 | ) { 74 | for (routeOption in routeOptions) { 75 | withPermission( 76 | action, 77 | getAuthorizationDetails(routeOption), 78 | validateOrgIdFromPath, 79 | ) { 80 | patch(routeOption.pathTemplate, body) 81 | } 82 | } 83 | } 84 | 85 | fun Route.postWithPermission( 86 | routeOptions: List, 87 | action: String, 88 | validateOrgIdFromPath: Boolean = true, 89 | body: suspend PipelineContext.(Unit) -> Unit, 90 | ) { 91 | for (routeOption in routeOptions) { 92 | withPermission( 93 | action, 94 | getAuthorizationDetails(routeOption), 95 | validateOrgIdFromPath, 96 | ) { 97 | post(routeOption.pathTemplate, body) 98 | } 99 | } 100 | } 101 | 102 | fun Route.deleteWithPermission( 103 | routeOptions: List, 104 | action: String, 105 | validateOrgIdFromPath: Boolean = true, 106 | body: suspend PipelineContext.(Unit) -> Unit, 107 | ) { 108 | for (routeOption in routeOptions) { 109 | withPermission( 110 | action, 111 | getAuthorizationDetails(routeOption), 112 | validateOrgIdFromPath, 113 | ) { 114 | delete(routeOption.pathTemplate, body) 115 | } 116 | } 117 | } 118 | 119 | data class RouteOption( 120 | val pathTemplate: String, 121 | val resourceNameIndex: Int, 122 | val resourceInstanceIndex: Int, 123 | val organizationIdIndex: Int? = null, 124 | val subOrganizationNameIndex: Int? = null, 125 | ) 126 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/extensions/SubOrganizationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.extensions 2 | 3 | import io.ktor.server.plugins.BadRequestException 4 | import java.util.Base64 5 | 6 | /** 7 | * For sub organizations, we have to encode the email address to include the org details in the email 8 | * so that users can have unique credentials across orgs. 9 | * 10 | * To support this, we are using the email local addressing scheme. This scheme allows us to add a suffix to email 11 | * address. 12 | * Ex: hello@hypto.in can be encoded as hello+@hypto.in 13 | * 14 | * With this option, same email address hello@hypto.in can configure two different passwords for orgId1 and orgId2. 15 | */ 16 | fun getEncodedEmail( 17 | organizationId: String, 18 | subOrganizationName: String?, 19 | email: String, 20 | ) = 21 | if (subOrganizationName != null) { 22 | encodeSubOrgUserEmail( 23 | email, 24 | organizationId, 25 | ) 26 | } else { 27 | email 28 | } 29 | 30 | private fun encodeSubOrgUserEmail( 31 | email: String, 32 | organizationId: String, 33 | ): String { 34 | val emailParts = email.split("@").takeIf { it.size == 2 } ?: throw BadRequestException("Invalid email address") 35 | val localPart = emailParts[0] 36 | val domainPart = emailParts[1] 37 | val subAddress = Base64.getEncoder().encodeToString(organizationId.toByteArray()) 38 | return "$localPart+$subAddress@$domainPart" 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/extensions/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.extensions 2 | 3 | import java.time.LocalDateTime 4 | import java.time.OffsetDateTime 5 | import java.time.ZoneOffset 6 | 7 | fun LocalDateTime.toUTCOffset(): OffsetDateTime { 8 | return this.atOffset(ZoneOffset.UTC) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/extensions/Validators.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.extensions 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.Constants.Companion.MAX_NAME_LENGTH 5 | import com.hypto.iam.server.Constants.Companion.MIN_LENGTH 6 | import com.hypto.iam.server.di.getKoinInstance 7 | import com.hypto.iam.server.utils.HrnFactory 8 | import com.hypto.iam.server.utils.HrnParseException 9 | import io.konform.validation.Invalid 10 | import io.konform.validation.Validation 11 | import io.konform.validation.ValidationBuilder 12 | import io.konform.validation.jsonschema.maxLength 13 | import io.konform.validation.jsonschema.minLength 14 | import io.konform.validation.jsonschema.pattern 15 | import java.time.LocalDateTime 16 | import java.time.format.DateTimeFormatter 17 | import java.time.format.DateTimeParseException 18 | import kotlin.reflect.KProperty1 19 | 20 | private val gson: Gson = getKoinInstance() 21 | 22 | fun ValidationBuilder.oneOf( 23 | instance: T, 24 | values: List>, 25 | ) = 26 | addConstraint("must have only one of the provided attributes") { 27 | println(values) 28 | return@addConstraint (values.mapNotNull { it.get(instance) }.size == 1) 29 | } 30 | 31 | fun ValidationBuilder.oneOrMoreOf( 32 | instance: T, 33 | values: List>, 34 | ) = 35 | addConstraint("must have at least one of the provided attributes") { 36 | return@addConstraint values.mapNotNull { it.get(instance) }.isNotEmpty() 37 | } 38 | 39 | enum class TimeNature { ANY, PAST, FUTURE } 40 | 41 | fun ValidationBuilder.dateTime( 42 | format: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME, 43 | nature: TimeNature = TimeNature.ANY, 44 | ) = addConstraint("must be a valid date time string") { 45 | try { 46 | val dateTime = LocalDateTime.parse(it, format) 47 | when (nature) { 48 | TimeNature.ANY -> { 49 | true 50 | } 51 | TimeNature.PAST -> { 52 | dateTime.isBefore(LocalDateTime.now()) 53 | } 54 | TimeNature.FUTURE -> { 55 | dateTime.isAfter(LocalDateTime.now()) 56 | } 57 | } 58 | } catch (e: DateTimeParseException) { 59 | false 60 | } 61 | } 62 | 63 | fun ValidationBuilder.hrn() = 64 | addConstraint("must be a valid hrn") { 65 | try { 66 | HrnFactory.getHrn(it) 67 | true 68 | } catch (e: HrnParseException) { 69 | false 70 | } 71 | } 72 | 73 | fun ValidationBuilder.noEndSpaces() = 74 | addConstraint("must not have spaces at either ends") { 75 | it.trim() == it 76 | } 77 | 78 | const val RESOURCE_NAME_REGEX = "^[a-zA-Z0-9_-]*\$" 79 | const val RESOURCE_NAME_REGEX_HINT = "Only characters A..Z, a..z, 0-9, _ and - are supported." 80 | val nameCheck = 81 | Validation { 82 | minLength(MIN_LENGTH) hint "Minimum length expected is $MIN_LENGTH" 83 | maxLength(MAX_NAME_LENGTH) hint "Maximum length supported for name is $MAX_NAME_LENGTH characters" 84 | pattern(RESOURCE_NAME_REGEX) hint RESOURCE_NAME_REGEX_HINT 85 | } 86 | 87 | /** 88 | * Extension method to throw exception when request object don't meet the constraint. 89 | */ 90 | fun Validation.validateAndThrowOnFailure(value: T): T { 91 | val result = validate(value) 92 | if (result is Invalid) { 93 | throw IllegalArgumentException(gson.toJson(result.errors)) 94 | } 95 | return value 96 | } 97 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/idp/Configuration.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.idp 2 | 3 | data class Configuration(val passwordPolicy: PasswordPolicy) 4 | 5 | data class PasswordPolicy( 6 | val minLength: Int = 8, 7 | val requireUpperCase: Boolean = true, 8 | val requireLowerCase: Boolean = true, 9 | val requireNumber: Boolean = true, 10 | val requireSymbols: Boolean = true, 11 | ) 12 | 13 | // TODO: Add configurations related to MFA, Email verification flows 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/idp/IdentityProvider.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.idp 2 | 3 | /** 4 | * Identity provider interface to manage users 5 | */ 6 | interface IdentityProvider { 7 | enum class IdentitySource { AWS_COGNITO } 8 | 9 | suspend fun createIdentityGroup( 10 | name: String, 11 | configuration: Configuration = Configuration(PasswordPolicy()), 12 | ): IdentityGroup 13 | 14 | suspend fun deleteIdentityGroup(identityGroup: IdentityGroup) 15 | 16 | suspend fun createUser( 17 | context: RequestContext, 18 | identityGroup: IdentityGroup, 19 | userCredentials: UserCredentials, 20 | ): User 21 | 22 | /** 23 | * Gets user details for the given username. If no user available with the username this will throw Exception 24 | */ 25 | suspend fun getUser( 26 | identityGroup: IdentityGroup, 27 | userName: String, 28 | isAliasUsername: Boolean = false, 29 | ): User 30 | 31 | suspend fun updateUser( 32 | identityGroup: IdentityGroup, 33 | userName: String, 34 | name: String?, 35 | phone: String?, 36 | status: com.hypto.iam.server.models.User.Status?, 37 | verified: Boolean?, 38 | ): User 39 | 40 | suspend fun listUsers( 41 | identityGroup: IdentityGroup, 42 | pageToken: String?, 43 | limit: Int?, 44 | ): Pair, NextToken?> 45 | 46 | suspend fun deleteUser( 47 | identityGroup: IdentityGroup, 48 | userName: String, 49 | ) 50 | 51 | suspend fun getIdentitySource(): IdentitySource 52 | 53 | suspend fun authenticate( 54 | identityGroup: IdentityGroup, 55 | userName: String, 56 | password: String, 57 | ): User 58 | 59 | suspend fun getUserByEmail( 60 | identityGroup: IdentityGroup, 61 | email: String, 62 | ): User 63 | 64 | suspend fun setUserPassword( 65 | identityGroup: IdentityGroup, 66 | userName: String, 67 | password: String, 68 | ) 69 | } 70 | 71 | typealias NextToken = String 72 | 73 | class UnsupportedCredentialsException(message: String) : Exception(message) 74 | 75 | class UserNotFoundException(message: String) : Exception(message) 76 | 77 | class UserAlreadyExistException(message: String) : Exception(message) 78 | 79 | data class IdentityGroup( 80 | val id: String, 81 | val name: String, 82 | val identitySource: IdentityProvider.IdentitySource, 83 | val metadata: Map = mapOf(), 84 | ) 85 | 86 | data class User( 87 | val username: String, 88 | val preferredUsername: String?, 89 | val name: String, 90 | val phoneNumber: String, 91 | val email: String, 92 | val loginAccess: Boolean, 93 | val isEnabled: Boolean, 94 | val createdBy: String, 95 | val verified: Boolean, 96 | val createdAt: String, 97 | ) 98 | 99 | interface UserCredentials { 100 | val username: String 101 | } 102 | 103 | data class PasswordCredentials( 104 | override val username: String, 105 | val name: String?, 106 | val email: String, 107 | val phoneNumber: String, 108 | val password: String, 109 | val preferredUsername: String?, 110 | ) : UserCredentials 111 | 112 | data class AccessTokenCredentials( 113 | override val username: String, 114 | val email: String, 115 | val phoneNumber: String, 116 | val accessToken: String, 117 | ) : UserCredentials 118 | 119 | data class RequestContext( 120 | val organizationId: String, 121 | val subOrganizationName: String?, 122 | val requestedPrincipal: String, 123 | val verified: Boolean, 124 | ) 125 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/lambdas/AuditEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.lambdas 2 | 3 | import com.amazonaws.services.lambda.runtime.Context 4 | import com.amazonaws.services.lambda.runtime.RequestHandler 5 | import com.amazonaws.services.lambda.runtime.events.SQSEvent 6 | import com.google.gson.Gson 7 | import com.hypto.iam.server.db.repositories.AuditEntriesRepo 8 | import com.hypto.iam.server.db.tables.pojos.AuditEntries 9 | import com.hypto.iam.server.di.getKoinInstance 10 | import kotlinx.coroutines.runBlocking 11 | 12 | class AuditEventHandler : RequestHandler { 13 | private val auditEntryRepo: AuditEntriesRepo = getKoinInstance() 14 | 15 | override fun handleRequest( 16 | input: SQSEvent, 17 | context: Context?, 18 | ) { 19 | val entries = input.records.map { auditEntriesFrom(it) } 20 | 21 | val state = 22 | runBlocking { 23 | auditEntryRepo.batchInsert(entries) 24 | } 25 | 26 | // TODO: Verify if the entry already exists in case of failed messages and ignore them. 27 | if (!state) { 28 | throw Exception("few Batch inserts failed") 29 | } 30 | } 31 | 32 | companion object { 33 | val gson: Gson = getKoinInstance() 34 | 35 | fun auditEntriesFrom(message: SQSEvent.SQSMessage): AuditEntries { 36 | return gson.fromJson(message.body, AuditEntries::class.java) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/plugins/globalcalldata/CallCache.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.plugins.globalcalldata 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | import kotlin.coroutines.CoroutineContext 5 | 6 | class CallCache { 7 | private val data = ConcurrentHashMap>() 8 | 9 | fun create(context: CoroutineContext) { 10 | data[context] = hashMapOf() 11 | } 12 | 13 | fun set( 14 | context: CoroutineContext, 15 | key: Key, 16 | value: V, 17 | ) { 18 | data[context]?.put(key, value) 19 | } 20 | 21 | fun get( 22 | context: CoroutineContext, 23 | key: Key, 24 | ): V? { 25 | return data[context]?.get(key) as V? 26 | } 27 | 28 | fun remove(context: CoroutineContext) { 29 | data.remove(context) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/plugins/globalcalldata/CallData.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.plugins.globalcalldata 2 | 3 | import io.ktor.server.application.ApplicationCall 4 | import kotlin.coroutines.CoroutineContext 5 | import kotlin.coroutines.coroutineContext 6 | 7 | interface Key 8 | 9 | object Call : Key 10 | 11 | open class CallData(context: CoroutineContext) { 12 | private val delegate = CallDataDelegate(context) 13 | val call: ApplicationCall by delegate.propNotNull(Call) 14 | } 15 | 16 | suspend fun callData(): CallData { 17 | if (!GlobalCallData.enabled) { 18 | throw IllegalAccessException("GlobalCallData Feature is not enabled!") 19 | } 20 | 21 | return CallData(coroutineContext) 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/plugins/globalcalldata/CallDataDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.plugins.globalcalldata 2 | 3 | import kotlin.coroutines.CoroutineContext 4 | import kotlin.reflect.KProperty 5 | 6 | class CallDataDelegate(private val context: CoroutineContext) { 7 | fun prop(key: Key): DelegateProperty { 8 | return DelegateProperty(key) 9 | } 10 | 11 | fun propNotNull(key: Key): DelegatePropertyNotNull { 12 | return DelegatePropertyNotNull(key) 13 | } 14 | 15 | inner class DelegateProperty(private val key: Key) { 16 | operator fun getValue( 17 | thisRef: Any?, 18 | property: KProperty<*>, 19 | ): V? { 20 | return GlobalCallData.callCache.get(context, key) 21 | } 22 | 23 | operator fun setValue( 24 | thisRef: Any?, 25 | property: KProperty<*>, 26 | value: V, 27 | ) { 28 | GlobalCallData.callCache.set(context, key, value) 29 | } 30 | } 31 | 32 | inner class DelegatePropertyNotNull(private val key: Key) { 33 | private val delegate = DelegateProperty(key) 34 | 35 | operator fun getValue( 36 | thisRef: Any?, 37 | property: KProperty<*>, 38 | ): V { 39 | return delegate.getValue(thisRef, property)!! 40 | } 41 | 42 | operator fun setValue( 43 | thisRef: Any?, 44 | property: KProperty<*>, 45 | value: V, 46 | ) { 47 | delegate.setValue(thisRef, property, value) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/plugins/globalcalldata/GlobalCallData.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.plugins.globalcalldata 2 | 3 | import io.ktor.server.application.ApplicationCall 4 | import io.ktor.server.application.ApplicationCallPipeline 5 | import io.ktor.server.application.BaseApplicationPlugin 6 | import io.ktor.server.application.call 7 | import io.ktor.server.response.ApplicationSendPipeline 8 | import io.ktor.util.AttributeKey 9 | import io.ktor.util.pipeline.PipelineContext 10 | import io.ktor.util.pipeline.PipelinePhase 11 | 12 | class GlobalCallData(configuration: Configuration) { 13 | class Configuration 14 | 15 | private fun interceptBeforeReceive(context: PipelineContext) { 16 | callCache.create(context.coroutineContext) 17 | callCache.set(context.coroutineContext, Call, context.call) 18 | } 19 | 20 | private fun interceptAfterSend(context: PipelineContext) { 21 | callCache.remove(context.coroutineContext) 22 | } 23 | 24 | /** 25 | * Installable feature for [GlobalCallData]. 26 | */ 27 | companion object Feature : BaseApplicationPlugin { 28 | override val key = AttributeKey("GlobalCallData") 29 | val callCache = CallCache() 30 | var enabled = false 31 | 32 | val globalCallDataPhase = PipelinePhase("GlobalCallData") 33 | val globalCallDataCleanupPhase = PipelinePhase("GlobalCallDataCleanup") 34 | 35 | override fun install( 36 | pipeline: ApplicationCallPipeline, 37 | configure: Configuration.() -> Unit, 38 | ): GlobalCallData { 39 | val configuration = Configuration().apply(configure) 40 | 41 | return GlobalCallData(configuration).also { callDataFeature -> 42 | pipeline.insertPhaseAfter(ApplicationCallPipeline.Features, globalCallDataPhase) 43 | pipeline.intercept(globalCallDataPhase) { 44 | callDataFeature.interceptBeforeReceive(this) 45 | } 46 | 47 | pipeline.sendPipeline.insertPhaseAfter(ApplicationSendPipeline.After, globalCallDataCleanupPhase) 48 | pipeline.sendPipeline.intercept(globalCallDataCleanupPhase) { 49 | callDataFeature.interceptAfterSend(this) 50 | } 51 | 52 | enabled = true 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/service/AuthProviderService.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.service 2 | 3 | import com.hypto.iam.server.db.repositories.AuthProviderRepo 4 | import com.hypto.iam.server.extensions.PaginationContext 5 | import com.hypto.iam.server.extensions.from 6 | import com.hypto.iam.server.models.AuthProvider 7 | import com.hypto.iam.server.models.AuthProviderPaginatedResponse 8 | import org.koin.core.component.KoinComponent 9 | import org.koin.core.component.inject 10 | 11 | class AuthProviderServiceImpl : KoinComponent, AuthProviderService { 12 | private val authProviderRepo: AuthProviderRepo by inject() 13 | 14 | override suspend fun listAuthProvider(context: PaginationContext): AuthProviderPaginatedResponse { 15 | val authProviders = authProviderRepo.fetchAuthProvidersPaginated(context) 16 | val newContext = PaginationContext.from(authProviders.lastOrNull()?.providerName, context) 17 | return AuthProviderPaginatedResponse( 18 | authProviders.map { AuthProvider.from(it) }, 19 | newContext.nextToken, 20 | newContext.toOptions(), 21 | ) 22 | } 23 | } 24 | 25 | interface AuthProviderService { 26 | suspend fun listAuthProvider( 27 | context: PaginationContext, 28 | ): AuthProviderPaginatedResponse 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/service/DatabaseFactory.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.service 2 | 3 | import com.hypto.iam.server.configs.AppConfig 4 | import com.hypto.iam.server.db.listeners.DeleteOrUpdateWithoutWhereListener 5 | import com.zaxxer.hikari.HikariConfig 6 | import com.zaxxer.hikari.HikariDataSource 7 | import org.jooq.Configuration 8 | import org.jooq.SQLDialect 9 | import org.jooq.impl.DefaultConfiguration 10 | import org.jooq.impl.DefaultExecuteListenerProvider 11 | import org.koin.core.component.KoinComponent 12 | import org.koin.core.component.inject 13 | import java.sql.Connection 14 | 15 | object DatabaseFactory : KoinComponent { 16 | private val appConfig: AppConfig by inject() 17 | 18 | val pool: HikariDataSource = 19 | HikariDataSource( 20 | HikariConfig().apply { 21 | driverClassName = "org.postgresql.Driver" 22 | jdbcUrl = appConfig.database.jdbcUrl 23 | maximumPoolSize = appConfig.database.maximumPoolSize 24 | minimumIdle = appConfig.database.minimumIdle 25 | isAutoCommit = appConfig.database.isAutoCommit 26 | transactionIsolation = appConfig.database.transactionIsolation 27 | username = appConfig.database.username 28 | password = appConfig.database.password 29 | }, 30 | ) 31 | 32 | private val daoConfiguration = 33 | DefaultConfiguration() 34 | .set(SQLDialect.POSTGRES) 35 | .set(pool) 36 | .deriveSettings { 37 | // https://www.jooq.org/doc/latest/manual/sql-building/dsl-context/custom-settings/settings-return-all-on-store/ 38 | it.withReturnAllOnUpdatableRecord(true) 39 | }.setAppending(DefaultExecuteListenerProvider(DeleteOrUpdateWithoutWhereListener())) 40 | 41 | fun getConfiguration(): Configuration { 42 | return daoConfiguration 43 | } 44 | 45 | fun getConnection(): Connection { 46 | return pool.connection 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/service/PolicyTemplatesService.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.service 2 | 3 | import com.hypto.iam.server.Constants.Companion.ORGANIZATION_ID_KEY 4 | import com.hypto.iam.server.Constants.Companion.USER_HRN_KEY 5 | import com.hypto.iam.server.db.repositories.PolicyTemplatesRepo 6 | import com.hypto.iam.server.db.repositories.RawPolicyPayload 7 | import com.hypto.iam.server.db.tables.records.PoliciesRecord 8 | import com.hypto.iam.server.models.User 9 | import com.hypto.iam.server.utils.IamResources 10 | import com.hypto.iam.server.utils.ResourceHrn 11 | import net.pwall.mustache.Template 12 | import net.pwall.mustache.parser.MustacheParserException 13 | import org.koin.core.component.KoinComponent 14 | import org.koin.core.component.inject 15 | 16 | class PolicyTemplatesServiceImpl : KoinComponent, PolicyTemplatesService { 17 | private val policyTemplateRepo: PolicyTemplatesRepo by inject() 18 | private val policyService: PolicyService by inject() 19 | 20 | override suspend fun createPersistAndReturnRootPolicyRecordsForOrganization( 21 | organizationId: String, 22 | user: User, 23 | ): List { 24 | val templateVariablesMap = mapOf(ORGANIZATION_ID_KEY to organizationId, USER_HRN_KEY to user.hrn) 25 | val policyTemplates = policyTemplateRepo.fetchActivePolicyTemplatesForOrgCreation() 26 | val adminPolicyNames = policyTemplates.mapNotNullTo(mutableSetOf()) { if (it.isRootPolicy) it.name else null } 27 | 28 | val rawPolicyPayloadsList = 29 | policyTemplates.map { 30 | val template = 31 | try { 32 | Template.parse(it.statements) 33 | } catch (e: MustacheParserException) { 34 | throw IllegalStateException("Invalid template ${it.name} - ${e.localizedMessage}", e) 35 | } 36 | RawPolicyPayload( 37 | hrn = ResourceHrn(organizationId, "", IamResources.POLICY, it.name), 38 | description = it.description, 39 | statements = template.processToString(templateVariablesMap), 40 | ) 41 | } 42 | 43 | val policies = policyService.batchCreatePolicyRaw(organizationId, rawPolicyPayloadsList) 44 | return policies.filter { 45 | adminPolicyNames.contains(ResourceHrn(it.hrn).resourceInstance) 46 | } 47 | } 48 | } 49 | 50 | interface PolicyTemplatesService { 51 | suspend fun createPersistAndReturnRootPolicyRecordsForOrganization( 52 | organizationId: String, 53 | user: User, 54 | ): List 55 | } 56 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/service/PrincipalPolicyService.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.service 2 | 3 | import com.hypto.iam.server.db.repositories.PoliciesRepo 4 | import com.hypto.iam.server.db.repositories.PrincipalPoliciesRepo 5 | import com.hypto.iam.server.db.tables.records.PrincipalPoliciesRecord 6 | import com.hypto.iam.server.models.BaseSuccessResponse 7 | import com.hypto.iam.server.utils.Hrn 8 | import com.hypto.iam.server.utils.ResourceHrn 9 | import com.hypto.iam.server.utils.policy.PolicyBuilder 10 | import com.hypto.iam.server.utils.policy.PolicyVariables 11 | import mu.KotlinLogging 12 | import org.koin.core.component.KoinComponent 13 | import org.koin.core.component.inject 14 | import java.time.LocalDateTime 15 | 16 | private val logger = KotlinLogging.logger {} 17 | 18 | class PrincipalPolicyServiceImpl : PrincipalPolicyService, KoinComponent { 19 | private val policiesRepo: PoliciesRepo by inject() 20 | private val principalPoliciesRepo: PrincipalPoliciesRepo by inject() 21 | 22 | override suspend fun fetchEntitlements(userHrn: String): PolicyBuilder { 23 | val principalPolicies = principalPoliciesRepo.fetchByPrincipalHrn(userHrn) 24 | val hrn = ResourceHrn(userHrn) 25 | val policyBuilder = PolicyBuilder().withPolicyVariables(PolicyVariables(organizationId = hrn.organization, userHrn = userHrn, userId = hrn.resourceInstance)) 26 | principalPolicies.forEach { 27 | val policy = policiesRepo.fetchByHrn(it.policyHrn)!! 28 | logger.info { policy.statements } 29 | 30 | policyBuilder.withPolicy(policy).withPrincipalPolicy(it) 31 | } 32 | 33 | return policyBuilder 34 | } 35 | 36 | override suspend fun attachPoliciesToUser( 37 | principal: Hrn, 38 | policies: List, 39 | ): BaseSuccessResponse { 40 | require(policies.isNotEmpty()) { 41 | "No policy Hrns provided to attach" 42 | } 43 | 44 | require(policiesRepo.existsByIds(policies.map { it.toString() })) { 45 | "Invalid policies found" 46 | } 47 | 48 | principalPoliciesRepo.insert( 49 | policies.map { 50 | PrincipalPoliciesRecord() 51 | .setPrincipalHrn(principal.toString()) 52 | .setPolicyHrn(it.toString()) 53 | .setCreatedAt(LocalDateTime.now()) 54 | }, 55 | ) 56 | 57 | return BaseSuccessResponse(true) 58 | } 59 | 60 | override suspend fun detachPoliciesToUser( 61 | principal: Hrn, 62 | policies: List, 63 | ): BaseSuccessResponse { 64 | principalPoliciesRepo.delete(principal, policies.map { it.toString() }) 65 | return BaseSuccessResponse(true) 66 | } 67 | } 68 | 69 | interface PrincipalPolicyService { 70 | suspend fun fetchEntitlements(userHrn: String): PolicyBuilder 71 | 72 | suspend fun attachPoliciesToUser( 73 | principal: Hrn, 74 | policies: List, 75 | ): BaseSuccessResponse 76 | 77 | suspend fun detachPoliciesToUser( 78 | principal: Hrn, 79 | policies: List, 80 | ): BaseSuccessResponse 81 | } 82 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/service/SubOrganizationService.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.service 2 | 3 | import com.hypto.iam.server.db.repositories.SubOrganizationRepo 4 | import com.hypto.iam.server.exceptions.EntityNotFoundException 5 | import com.hypto.iam.server.extensions.PaginationContext 6 | import com.hypto.iam.server.extensions.from 7 | import com.hypto.iam.server.models.BaseSuccessResponse 8 | import com.hypto.iam.server.models.SubOrganization 9 | import com.hypto.iam.server.models.SubOrganizationsPaginatedResponse 10 | import org.koin.core.component.KoinComponent 11 | import org.koin.core.component.inject 12 | 13 | class SubOrganizationServiceImpl : KoinComponent, SubOrganizationService { 14 | private val subOrganizationRepo: SubOrganizationRepo by inject() 15 | 16 | override suspend fun createSubOrganization( 17 | organizationId: String, 18 | subOrganizationName: String, 19 | description: String?, 20 | ): SubOrganization { 21 | val subOrganizationRecord = subOrganizationRepo.create(organizationId, subOrganizationName, description) 22 | return SubOrganization.from(subOrganizationRecord) 23 | } 24 | 25 | override suspend fun getSubOrganization( 26 | organizationId: String, 27 | subOrganizationName: String, 28 | ): SubOrganization { 29 | val subOrganizationRecord = 30 | subOrganizationRepo.fetchById(organizationId, subOrganizationName) ?: throw EntityNotFoundException( 31 | "SubOrganization with " + 32 | "name [$subOrganizationName] not found", 33 | ) 34 | return SubOrganization.from(subOrganizationRecord) 35 | } 36 | 37 | override suspend fun listSubOrganizations( 38 | organizationId: String, 39 | context: PaginationContext, 40 | ): SubOrganizationsPaginatedResponse { 41 | val subOrganizationsRecord = subOrganizationRepo.fetchSubOrganizationsPaginated(organizationId, context) 42 | val newContext = PaginationContext.from(subOrganizationsRecord.lastOrNull()?.name, context) 43 | return SubOrganizationsPaginatedResponse( 44 | subOrganizationsRecord.map { SubOrganization.from(it) }, 45 | newContext.nextToken, 46 | newContext.toOptions(), 47 | ) 48 | } 49 | 50 | override suspend fun updateSubOrganization( 51 | organizationId: String, 52 | subOrganizationName: String, 53 | updatedDescription: String?, 54 | ): SubOrganization { 55 | val subOrganizationRecord = 56 | subOrganizationRepo 57 | .update(organizationId, subOrganizationName, updatedDescription) 58 | ?: throw EntityNotFoundException("SubOrganization with id [$subOrganizationName] not found") 59 | return SubOrganization.from(subOrganizationRecord) 60 | } 61 | 62 | override suspend fun deleteSubOrganization( 63 | organizationId: String, 64 | subOrganizationName: String, 65 | ): BaseSuccessResponse { 66 | subOrganizationRepo.delete(organizationId, subOrganizationName) 67 | return BaseSuccessResponse(true) 68 | } 69 | } 70 | 71 | interface SubOrganizationService { 72 | suspend fun createSubOrganization( 73 | organizationId: String, 74 | subOrganizationName: String, 75 | description: String?, 76 | ): SubOrganization 77 | 78 | suspend fun getSubOrganization( 79 | organizationId: String, 80 | subOrganizationName: String, 81 | ): SubOrganization 82 | 83 | suspend fun listSubOrganizations( 84 | organizationId: String, 85 | context: PaginationContext, 86 | ): SubOrganizationsPaginatedResponse 87 | 88 | suspend fun updateSubOrganization( 89 | organizationId: String, 90 | subOrganizationName: String, 91 | updatedDescription: String?, 92 | ): SubOrganization 93 | 94 | suspend fun deleteSubOrganization( 95 | organizationId: String, 96 | subOrganizationName: String, 97 | ): BaseSuccessResponse 98 | } 99 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/service/UserAuthService.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.service 2 | 3 | import com.hypto.iam.server.authProviders.AuthProviderRegistry 4 | import com.hypto.iam.server.db.repositories.OrganizationRepo 5 | import com.hypto.iam.server.db.repositories.UserAuthRepo 6 | import com.hypto.iam.server.db.repositories.UserRepo 7 | import com.hypto.iam.server.exceptions.EntityNotFoundException 8 | import com.hypto.iam.server.models.BaseSuccessResponse 9 | import com.hypto.iam.server.models.UserAuthMethod 10 | import com.hypto.iam.server.models.UserAuthMethodsResponse 11 | import com.hypto.iam.server.security.AuthMetadata 12 | import com.hypto.iam.server.security.TokenCredential 13 | import com.hypto.iam.server.security.TokenType 14 | import com.hypto.iam.server.security.UserPrincipal 15 | import com.hypto.iam.server.utils.IamResources 16 | import com.hypto.iam.server.utils.ResourceHrn 17 | import io.ktor.server.plugins.BadRequestException 18 | import org.koin.core.component.KoinComponent 19 | import org.koin.core.component.inject 20 | 21 | class UserAuthServiceImpl : KoinComponent, UserAuthService { 22 | private val organizationRepo: OrganizationRepo by inject() 23 | private val userRepo: UserRepo by inject() 24 | private val userAuthRepo: UserAuthRepo by inject() 25 | 26 | override suspend fun createUserAuth( 27 | organizationId: String, 28 | userId: String, 29 | issuer: String, 30 | token: String, 31 | principal: UserPrincipal, 32 | ): BaseSuccessResponse { 33 | val authProvider = 34 | AuthProviderRegistry.getProvider(issuer) ?: throw BadRequestException( 35 | "Invalid issuer", 36 | ) 37 | val oAuthUserPrincipal = authProvider.getProfileDetails(TokenCredential(token, TokenType.OAUTH)) 38 | val user = 39 | userRepo.findByEmail(oAuthUserPrincipal.email) 40 | ?: throw BadRequestException("User not found") 41 | require(principal.hrnStr == user.hrn) { 42 | "Can't add auth method for another user" 43 | } 44 | userAuthRepo.fetchByUserHrnAndProviderName(user.hrn, issuer) ?: userAuthRepo.create( 45 | user.hrn, 46 | issuer, 47 | oAuthUserPrincipal.metadata?.let { AuthMetadata.toJsonB(it) }, 48 | ) 49 | return BaseSuccessResponse(true) 50 | } 51 | 52 | override suspend fun listUserAuth( 53 | organizationId: String, 54 | userId: String, 55 | ): UserAuthMethodsResponse { 56 | organizationRepo.findById(organizationId) 57 | ?: throw EntityNotFoundException("Invalid organization id") 58 | val userAuth = 59 | userAuthRepo.fetchUserAuth( 60 | ResourceHrn(organizationId, "", IamResources.USER, userId), 61 | ).map { 62 | UserAuthMethod( 63 | providerName = it.providerName, 64 | ) 65 | }.toList() 66 | 67 | return UserAuthMethodsResponse(userAuth) 68 | } 69 | } 70 | 71 | interface UserAuthService { 72 | suspend fun createUserAuth( 73 | organizationId: String, 74 | userId: String, 75 | issuer: String, 76 | token: String, 77 | principal: UserPrincipal, 78 | ): BaseSuccessResponse 79 | 80 | suspend fun listUserAuth( 81 | organizationId: String, 82 | userHrn: String, 83 | ): UserAuthMethodsResponse 84 | } 85 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/service/ValidationService.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.service 2 | 3 | import com.hypto.iam.server.extensions.from 4 | import com.hypto.iam.server.models.ResourceActionEffect 5 | import com.hypto.iam.server.models.ResourceActionEffect.Effect 6 | import com.hypto.iam.server.models.ValidationRequest 7 | import com.hypto.iam.server.models.ValidationResponse 8 | import com.hypto.iam.server.utils.Hrn 9 | import com.hypto.iam.server.utils.policy.PolicyRequest 10 | import com.hypto.iam.server.utils.policy.PolicyValidator 11 | import org.koin.core.component.KoinComponent 12 | import org.koin.core.component.inject 13 | 14 | class ValidationServiceImpl : ValidationService, KoinComponent { 15 | private val principalPolicyService: PrincipalPolicyService by inject() 16 | private val policyValidator: PolicyValidator by inject() 17 | 18 | override suspend fun validateIfUserHasPermissionToActions( 19 | principalHrn: Hrn, 20 | validationRequest: ValidationRequest, 21 | ): ValidationResponse { 22 | val policyBuilder = principalPolicyService.fetchEntitlements(principalHrn.toString()) 23 | val validations = validationRequest.validations 24 | 25 | val results = 26 | policyValidator.batchValidate( 27 | policyBuilder, 28 | validations.map { PolicyRequest(principalHrn.toString(), it.resource, it.action) }, 29 | ) 30 | 31 | return ValidationResponse( 32 | results.mapIndexed { i, isValid -> 33 | ResourceActionEffect.from( 34 | validations[i], 35 | if (isValid) { 36 | Effect.allow 37 | } else { 38 | Effect.deny 39 | }, 40 | ) 41 | }, 42 | ) 43 | } 44 | } 45 | 46 | interface ValidationService { 47 | suspend fun validateIfUserHasPermissionToActions( 48 | principalHrn: Hrn, 49 | validationRequest: ValidationRequest, 50 | ): ValidationResponse 51 | } 52 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/utils/ApplicationIdUtil.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.utils 2 | 3 | import com.hypto.iam.server.utils.IdGenerator.Charset 4 | import com.hypto.iam.server.validators.resourceNameCheck 5 | import io.konform.validation.Valid 6 | import org.koin.core.component.KoinComponent 7 | import org.koin.core.component.inject 8 | 9 | object ApplicationIdUtil : KoinComponent { 10 | private const val ORGANIZATION_ID_LENGTH = 10L 11 | private const val REFRESH_TOKEN_RANDOM_LENGTH = 30L 12 | private const val REQUEST_ID_LENGTH = 15L 13 | private const val PASSCODE_ID_LENGTH = 10L 14 | 15 | object Generator { 16 | private val idGenerator: IdGenerator by inject() 17 | 18 | fun organizationId(): String { 19 | return idGenerator.randomId(ORGANIZATION_ID_LENGTH, Charset.ALPHANUMERIC) 20 | } 21 | 22 | // First 10 chars: alphabets (upper) representing organizationId 23 | // next 20 chars: alphanumeric with upper and lower case - random 24 | fun refreshToken(organizationId: String): String { 25 | return organizationId + idGenerator.timeBasedRandomId(REFRESH_TOKEN_RANDOM_LENGTH, Charset.ALPHABETS) 26 | } 27 | 28 | fun requestId(): String { 29 | return idGenerator.timeBasedRandomId(REQUEST_ID_LENGTH, Charset.ALPHANUMERIC) 30 | } 31 | 32 | fun passcodeId(): String { 33 | return idGenerator.timeBasedRandomId(PASSCODE_ID_LENGTH, Charset.ALPHANUMERIC) 34 | } 35 | 36 | fun username(): String { 37 | return idGenerator.randomUUID() 38 | } 39 | } 40 | 41 | object Validator { 42 | fun organizationId(orgId: String): Boolean { 43 | return (orgId.length == ORGANIZATION_ID_LENGTH.toInt() && orgId.all { it.isUpperCase() }) 44 | } 45 | 46 | fun name(name: String): Boolean { 47 | return resourceNameCheck(name) is Valid 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/utils/EncryptUtil.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.utils 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.service.MasterKeyCache 5 | import org.koin.core.component.KoinComponent 6 | import org.koin.core.component.inject 7 | import java.util.Base64 8 | import javax.crypto.Cipher 9 | import javax.crypto.spec.GCMParameterSpec 10 | import javax.crypto.spec.SecretKeySpec 11 | 12 | data class EncryptedData( 13 | val data: String, 14 | val keyId: String, 15 | ) 16 | 17 | object EncryptUtil : KoinComponent { 18 | private const val ALGORITHM = "AES/GCM/NoPadding" 19 | private const val KEY_ALGORITHM = "AES" 20 | private const val TAG_LENGTH_BIT = 128 21 | private const val KEY_LENGTH = 16 22 | private val gcm: GCMParameterSpec 23 | private val masterKeyCache: MasterKeyCache by inject() 24 | private val gson: Gson by inject() 25 | 26 | init { 27 | val nonce = ByteArray(KEY_LENGTH) 28 | gcm = GCMParameterSpec(TAG_LENGTH_BIT, nonce) 29 | } 30 | 31 | suspend fun encrypt(input: String): String { 32 | val cipher = Cipher.getInstance(ALGORITHM) 33 | val (key, keyId) = getSecretKeySpec() 34 | cipher.init(Cipher.ENCRYPT_MODE, key, gcm) 35 | val cipherText = cipher.doFinal(input.toByteArray()) 36 | return gson.toJson( 37 | EncryptedData( 38 | data = Base64.getEncoder().encodeToString(cipherText), 39 | keyId = keyId, 40 | ), 41 | ) 42 | } 43 | 44 | suspend fun decrypt(cipherText: String): String { 45 | val cipher = Cipher.getInstance(ALGORITHM) 46 | val encryptedData = gson.fromJson(cipherText, EncryptedData::class.java) 47 | val (key, _) = getSecretKeySpec(encryptedData.keyId) 48 | cipher.init(Cipher.DECRYPT_MODE, key, gcm) 49 | val plainText = cipher.doFinal(Base64.getDecoder().decode(encryptedData.data)) 50 | return String(plainText) 51 | } 52 | 53 | private suspend fun getSecretKeySpec(id: String? = null): Pair { 54 | val masterKey = 55 | if (id == null) masterKeyCache.forSigning() else masterKeyCache.getKey(id) 56 | val key = SecretKeySpec(masterKey.privateKey.toString().take(KEY_LENGTH).toByteArray(), KEY_ALGORITHM) 57 | return Pair(key, masterKey.id) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/utils/IamResources.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.utils 2 | 3 | object IamResources { 4 | const val ORGANIZATION = "iam-organization" 5 | const val ACCOUNT = "iam-account" 6 | const val USER = "iam-user" 7 | const val POLICY = "iam-policy" 8 | const val RESOURCE = "iam-resource" 9 | const val ROLE = "iam-role" 10 | const val ACTION = "iam-action" 11 | const val CREDENTIAL = "iam-credential" 12 | const val SUB_ORGANIZATION = "iam-sub-organization" 13 | const val USER_LINK = "iam-user-link" 14 | 15 | val resourceMap: Map = 16 | mapOf( 17 | "users" to USER, 18 | "resources" to RESOURCE, 19 | "policies" to POLICY, 20 | "actions" to ACTION, 21 | "credentials" to CREDENTIAL, 22 | "roles" to ROLE, 23 | "organizations" to ORGANIZATION, 24 | "accounts" to ACCOUNT, 25 | "sub_organizations" to SUB_ORGANIZATION, 26 | "user_links" to USER_LINK, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/utils/IdGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.utils 2 | 3 | import java.time.Instant 4 | import java.util.UUID 5 | import java.util.concurrent.ThreadLocalRandom 6 | import kotlin.streams.asSequence 7 | 8 | object IdGenerator { 9 | enum class Charset(val seed: String) { 10 | UPPERCASE_ALPHABETS("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), 11 | LOWERCASE_ALPHABETS(UPPERCASE_ALPHABETS.seed.lowercase()), 12 | NUMERIC("0123456789"), 13 | LOWER_ALPHANUMERIC(LOWERCASE_ALPHABETS.seed + NUMERIC.seed), 14 | UPPER_ALPHANUMERIC(UPPERCASE_ALPHABETS.seed + NUMERIC.seed), 15 | ALPHABETS(UPPERCASE_ALPHABETS.seed + LOWERCASE_ALPHABETS.seed), 16 | ALPHANUMERIC(ALPHABETS.seed + NUMERIC.seed), 17 | } 18 | 19 | fun randomId( 20 | length: Long = 10, 21 | charset: Charset = Charset.UPPERCASE_ALPHABETS, 22 | ): String { 23 | return ThreadLocalRandom.current().ints(length, 0, charset.seed.length) 24 | .asSequence() 25 | .map(charset.seed::get) 26 | .joinToString("") 27 | } 28 | 29 | /** 30 | * Convert numbers to required charset. 31 | * 32 | * E.g: For number = 1645204287 33 | * - numberToId(number, IdGenerator.Charset.NUMERIC) == "1645204287" 34 | * - numberToId(number, IdGenerator.Charset.ALPHABETS) == "ERAhbz" 35 | * - numberToId(number, IdGenerator.Charset.ALPHANUMERIC) == "BxVGxB" 36 | * - numberToId(number, IdGenerator.Charset.UPPERCASE_ALPHABETS) == "FIMFEDZ" 37 | * - numberToId(number, IdGenerator.Charset.LOWERCASE_ALPHABETS) == "fimfedz" 38 | * - numberToId(number, IdGenerator.Charset.UPPER_ALPHANUMERIC) == "1HSP1D" 39 | * - numberToId(number, IdGenerator.Charset.LOWER_ALPHANUMERIC) == "1hsp1d" 40 | */ 41 | fun numberToId( 42 | number: Long, 43 | charset: Charset = Charset.UPPERCASE_ALPHABETS, 44 | ): String { 45 | return if (number < 0L) { 46 | "-" + numberToId(-number - 1) 47 | } else if (number == 0L) { 48 | charset.seed[number.toInt()].toString() 49 | } else { 50 | val seed = charset.seed 51 | var quot = number 52 | val builder = StringBuilder() 53 | 54 | while (quot != 0L) { 55 | builder.append(seed[(quot % seed.length).toInt()]) 56 | quot /= seed.length 57 | } 58 | builder.reverse().toString() 59 | } 60 | } 61 | 62 | private const val MIN_TIME_BASED_RANDOM_ID_SIZE = 10 63 | 64 | fun timeBasedRandomId( 65 | length: Long = 10, 66 | charset: Charset = Charset.UPPERCASE_ALPHABETS, 67 | ): String { 68 | require(length >= MIN_TIME_BASED_RANDOM_ID_SIZE) { 69 | "Cannot generate a timestamp based random id with less than $MIN_TIME_BASED_RANDOM_ID_SIZE characters" 70 | } 71 | val timeId = numberToId(Instant.now().toEpochMilli(), charset) 72 | 73 | return timeId + randomId(length - timeId.length, charset) 74 | } 75 | 76 | fun randomUUID(): String { 77 | return UUID.randomUUID().toString() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/utils/MasterKeyUtil.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.utils 2 | 3 | import java.io.BufferedReader 4 | import java.io.File 5 | import java.io.IOException 6 | import java.io.InputStreamReader 7 | import java.nio.file.Files 8 | 9 | object MasterKeyUtil { 10 | private const val PRIVATE_KEY = "/tmp/private_key" 11 | private const val PUBLIC_KEY = "/tmp/public_key" 12 | 13 | /** 14 | * Generates new EC public & private key pair (both pem and der) in `/tmp` 15 | */ 16 | fun generateKeyPair() { 17 | try { 18 | val scriptStream = this::class.java.getResourceAsStream("/generate_key_pair.sh") 19 | requireNotNull(scriptStream) { 20 | "Script not found" 21 | } 22 | val file = File("/tmp/generate_key_pair.sh") 23 | Files.copy(scriptStream, file.toPath()) 24 | 25 | val process = Runtime.getRuntime().exec("/bin/bash ${file.absolutePath}", null, null) 26 | val output = StringBuilder() 27 | val reader = BufferedReader(InputStreamReader(process.inputStream)) 28 | 29 | var line: String? = reader.readLine() 30 | while (line != null) { 31 | output.append(line.trimIndent()) 32 | line = reader.readLine() 33 | } 34 | 35 | if (process.waitFor() == 0) { 36 | println(output) 37 | } else { 38 | throw IOException() 39 | } 40 | } catch (e: IOException) { 41 | println(e.message) 42 | } catch (e: InterruptedException) { 43 | println(e.message) 44 | } 45 | } 46 | 47 | fun loadPrivateKeyDer(): ByteArray { 48 | return loadKeyDer("$PRIVATE_KEY.der") 49 | } 50 | 51 | fun loadPublicKeyDer(): ByteArray { 52 | return loadKeyDer("$PUBLIC_KEY.der") 53 | } 54 | 55 | fun loadPublicKeyPem(): ByteArray { 56 | return loadKeyPem("$PUBLIC_KEY.pem") 57 | } 58 | 59 | fun loadPrivateKeyPem(): ByteArray { 60 | return loadKeyPem("$PRIVATE_KEY.pem") 61 | } 62 | 63 | private fun loadKeyDer(path: String): ByteArray { 64 | return File(path).readBytes() 65 | } 66 | 67 | private fun loadKeyPem(path: String): ByteArray { 68 | return File(path).readBytes() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/utils/Timing.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.utils 2 | 3 | import com.hypto.iam.server.di.getKoinInstance 4 | import io.micrometer.core.instrument.MeterRegistry 5 | import mu.KLogger 6 | import kotlin.time.ExperimentalTime 7 | import kotlin.time.measureTimedValue 8 | import kotlin.time.toJavaDuration 9 | 10 | val microMeterRegistry: MeterRegistry = getKoinInstance() 11 | 12 | inline fun measureTime( 13 | name: String, 14 | logger: KLogger, 15 | block: () -> Unit, 16 | ) = measureTimedValue(name, logger, block) 17 | 18 | @OptIn(ExperimentalTime::class) 19 | inline fun measureTimedValue( 20 | name: String, 21 | logger: KLogger, 22 | block: () -> T, 23 | ): T { 24 | val timedValue = measureTimedValue(block) 25 | microMeterRegistry.timer(name).record(timedValue.duration.toJavaDuration()) 26 | logger.info { "[Timing] Operation=[$name] | Time=[${timedValue.duration.inWholeNanoseconds} ns]" } 27 | return timedValue.value 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/utils/policy/PolicyRequest.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.utils.policy 2 | 3 | data class PolicyRequest(val principal: String, val resource: String, val action: String) 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/utils/policy/PolicyStatement.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("MagicNumber") 2 | 3 | package com.hypto.iam.server.utils.policy 4 | 5 | open class PolicyStatement(private val statement: String) { 6 | companion object { 7 | fun p( 8 | principal: String, 9 | resource: String, 10 | action: String, 11 | effect: String, 12 | ): PolicyStatement { 13 | return PolicyStatement("p, $principal, $resource, $action, $effect") 14 | } 15 | 16 | fun g( 17 | principal: String, 18 | policy: String, 19 | ): PolicyStatement { 20 | return PolicyStatement("g, $principal, $policy") 21 | } 22 | 23 | fun of( 24 | principal: String, 25 | statement: com.hypto.iam.server.models.PolicyStatement, 26 | ): PolicyStatement { 27 | return this.p(principal, statement.resource, statement.action, statement.effect.value) 28 | } 29 | } 30 | 31 | override fun toString(): String { 32 | return statement 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/utils/policy/PolicyValidator.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.utils.policy 2 | 3 | import org.casbin.jcasbin.main.CoreEnforcer.newModel 4 | import org.casbin.jcasbin.main.Enforcer 5 | import org.casbin.jcasbin.persist.file_adapter.FileAdapter 6 | import java.io.InputStream 7 | 8 | object PolicyValidator { 9 | private val modelStream = this::class.java.getResourceAsStream("/casbin_model.conf") 10 | private val model = modelStream?.let { newModel(String(it.readAllBytes(), Charsets.UTF_8)) } 11 | 12 | init { 13 | requireNotNull(modelStream) { "casbin_model.conf not found in resources" } 14 | } 15 | 16 | fun validate( 17 | policyBuilder: PolicyBuilder, 18 | policyRequest: PolicyRequest, 19 | ): Boolean { 20 | return validate(policyBuilder.stream(), policyRequest) 21 | } 22 | 23 | fun validate( 24 | inputStream: InputStream, 25 | policyRequest: PolicyRequest, 26 | ): Boolean { 27 | return validate(inputStream, listOf(policyRequest)) 28 | } 29 | 30 | fun validate( 31 | inputStream: InputStream, 32 | policyRequests: List, 33 | ): Boolean { 34 | return policyRequests 35 | .all { 36 | Enforcer(model, FileAdapter(inputStream)) 37 | .enforce(it.principal, it.resource, it.action) 38 | } 39 | } 40 | 41 | fun validate( 42 | enforcer: Enforcer, 43 | policyRequest: PolicyRequest, 44 | ): Boolean { 45 | return enforcer 46 | .enforce(policyRequest.principal, policyRequest.resource, policyRequest.action) 47 | } 48 | 49 | fun validateAny( 50 | inputStream: InputStream, 51 | policyRequests: List, 52 | ): Boolean { 53 | return policyRequests 54 | .any { 55 | Enforcer(model, FileAdapter(inputStream)) 56 | .enforce(it.principal, it.resource, it.action) 57 | } 58 | } 59 | 60 | fun validateNone( 61 | inputStream: InputStream, 62 | policyRequests: List, 63 | ): Boolean { 64 | return policyRequests 65 | .none { 66 | Enforcer(model, FileAdapter(inputStream)) 67 | .enforce(it.principal, it.resource, it.action) 68 | } 69 | } 70 | 71 | fun batchValidate( 72 | policyBuilder: PolicyBuilder, 73 | policyRequests: List, 74 | ): List { 75 | val enforcer = Enforcer(model, FileAdapter(policyBuilder.stream())) 76 | return policyRequests.map { validate(enforcer, it) }.toList() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hypto/iam/server/validators/MetadataValidator.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.validators 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.di.getKoinInstance 5 | import com.hypto.iam.server.extensions.hrn 6 | import com.hypto.iam.server.extensions.validateAndThrowOnFailure 7 | import io.konform.validation.Validation 8 | 9 | // This file contains extension functions to validate provided metadata 10 | private val gson: Gson = getKoinInstance() 11 | 12 | data class SignUpMetadata( 13 | val name: String, 14 | val description: String?, 15 | val rootUserPassword: String, 16 | val rootUserName: String?, 17 | val rootUserPreferredUsername: String?, 18 | val rootUserPhone: String?, 19 | ) 20 | 21 | data class InviteMetadata(val map: Map) { 22 | val inviterUserHrn: String by map 23 | val policies: List by map 24 | } 25 | 26 | data class RequestAccessMetadata(val map: Map) { 27 | val inviterUserHrn: String by map 28 | } 29 | 30 | data class VerifyEmailSignUpMetadata( 31 | val metadata: SignUpMetadata, 32 | ) 33 | 34 | data class VerifyEmailInviteMetadata( 35 | val metadata: InviteMetadata, 36 | ) 37 | 38 | val signUpMetadataValidation = 39 | Validation { 40 | VerifyEmailSignUpMetadata::metadata { 41 | SignUpMetadata::name required { 42 | run(orgNameCheck) 43 | } 44 | SignUpMetadata::rootUserPassword required { 45 | run(passwordCheck) 46 | } 47 | SignUpMetadata::rootUserName ifPresent { 48 | run(nameOfUserCheck) 49 | } 50 | SignUpMetadata::rootUserPreferredUsername ifPresent { 51 | run(preferredUserNameCheck) 52 | } 53 | SignUpMetadata::rootUserPhone ifPresent { 54 | run(phoneNumberCheck) 55 | } 56 | } 57 | } 58 | 59 | val inviteMetadataValidation = 60 | Validation { 61 | InviteMetadata::inviterUserHrn required { 62 | run(hrnCheck) 63 | } 64 | InviteMetadata::policies onEach { hrn() } 65 | } 66 | 67 | fun validateSignupMetadata(metadata: Map) { 68 | val metadataObject = gson.fromJson(gson.toJsonTree(metadata), SignUpMetadata::class.java) 69 | signUpMetadataValidation.validateAndThrowOnFailure(VerifyEmailSignUpMetadata(metadataObject)) 70 | } 71 | 72 | fun validateInviteMetadata(metadata: Map) { 73 | val metadataObject = InviteMetadata(metadata) 74 | inviteMetadataValidation.validateAndThrowOnFailure(metadataObject) 75 | } 76 | -------------------------------------------------------------------------------- /src/main/resources/casbin_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = principal, resource, operation 3 | 4 | [policy_definition] 5 | p = principal, resource, operation, eft 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) 12 | 13 | [matchers] 14 | m = g(r.principal, p.principal) && regexMatch(r.resource, p.resource) && regexMatch(r.operation, p.operation) 15 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V10__add_metadata_to_passcode.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE passcodes 2 | ADD COLUMN metadata text DEFAULT NULL; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V11__add_detail_columns_to_users.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ADD COLUMN name VARCHAR(50), 3 | ADD COLUMN created_by VARCHAR(200), 4 | ADD COLUMN login_access BOOLEAN DEFAULT NULL; 5 | ALTER TABLE users 6 | ALTER COLUMN email DROP NOT NULL; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V12__add_policy_template_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE policy_templates ( 2 | name VARCHAR(512) PRIMARY KEY, 3 | status VARCHAR(512) NOT NULL, -- ACTIVE, ARCHIVED 4 | is_root_policy BOOLEAN NOT NULL, 5 | statements text NOT NULL, 6 | 7 | created_at timestamp NOT NULL, 8 | updated_at timestamp NOT NULL 9 | ); 10 | 11 | 12 | 13 | INSERT INTO policy_templates values ( 14 | 'admin', 15 | 'ACTIVE', 16 | TRUE, 17 | 'p, hrn:{{organization_id}}::iam-policy/admin, ^hrn:{{organization_id}}$, hrn:{{organization_id}}:*, allow 18 | p, hrn:{{organization_id}}::iam-policy/admin, ^hrn:{{organization_id}}::*, hrn:{{organization_id}}::*, allow 19 | ', 20 | 'NOW()', 21 | 'NOW()' 22 | ); 23 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V13__add_description_to_policy_and_templates.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE policy_templates ADD COLUMN description VARCHAR(512) NULL; 2 | 3 | ALTER TABLE policies ADD COLUMN description VARCHAR(512) NULL; 4 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V14__create_auth_provider_table.sql: -------------------------------------------------------------------------------- 1 | -- OAuth Grant Type Enum 2 | CREATE TYPE grant_type AS ENUM ( 3 | 'CODE_AUTHORIZATION', 4 | 'CUSTOM', 5 | 'IMPLICIT' 6 | ); 7 | 8 | -- OAuth Provider Table 9 | CREATE TABLE auth_provider ( 10 | provider_name VARCHAR(512) NOT NULL PRIMARY KEY, 11 | auth_url VARCHAR(512) NOT NULL, 12 | client_id VARCHAR(512) NOT NULL, 13 | client_secret VARCHAR(512) NOT NULL, 14 | created_at timestamp NOT NULL, 15 | updated_at timestamp NOT NULL 16 | ); 17 | 18 | COMMENT ON TABLE auth_provider 19 | IS 'OAuth Provider Table'; 20 | COMMENT ON COLUMN auth_provider.provider_name 21 | IS 'OAuth Provider Name (e.g. Google, Microsoft)'; 22 | COMMENT ON COLUMN auth_provider.auth_url 23 | IS 'OAuth Authorization URL that frontend consumes to redirect user'; 24 | COMMENT ON COLUMN auth_provider.client_id 25 | IS 'OAuth Client ID generated at OAuth Provider to include in request'; 26 | COMMENT ON COLUMN auth_provider.client_secret 27 | IS 'OAuth Client Secret generated at OAuth Provider to obtain refresh token'; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V15__create_user_auth_table.sql: -------------------------------------------------------------------------------- 1 | -- Authorization methods of User Table 2 | CREATE TABLE user_auth ( 3 | user_hrn VARCHAR(512) NOT NULL, 4 | provider_name VARCHAR(512) NOT NULL, 5 | auth_metadata JSONB, 6 | created_at timestamp NOT NULL, 7 | updated_at timestamp NOT NULL, 8 | PRIMARY KEY(user_hrn, provider_name) 9 | ); 10 | 11 | COMMENT ON TABLE user_auth 12 | IS 'Authorization methods of User Table'; 13 | COMMENT ON COLUMN user_auth.user_hrn 14 | IS 'User HRN'; 15 | COMMENT ON COLUMN user_auth.provider_name 16 | IS 'OAuth Provider Name (e.g. Google, Microsoft)'; 17 | COMMENT ON COLUMN user_auth.auth_metadata 18 | IS 'OAuth Authorization Metadata (e.g. access_token, refresh_token, token_type, expires_in, scope)'; 19 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V16__create_sub_org_table.sql: -------------------------------------------------------------------------------- 1 | -- Sub organizations table 2 | CREATE TABLE sub_organizations ( 3 | name VARCHAR(1200) NOT NULL, 4 | organization_id VARCHAR(512) NOT NULL, 5 | description text, 6 | created_at timestamp NOT NULL, 7 | updated_at timestamp NOT NULL, 8 | 9 | PRIMARY KEY (name, organization_id), 10 | FOREIGN KEY (organization_id) REFERENCES organizations (id) 11 | ); 12 | 13 | COMMENT ON TABLE sub_organizations 14 | IS 'Table to store all sub organization details'; 15 | COMMENT ON COLUMN sub_organizations.name 16 | IS 'name of the sub organization'; 17 | COMMENT ON COLUMN sub_organizations.description 18 | IS 'description of the sub organization'; 19 | 20 | 21 | -- Add sub organization id to the users table 22 | ALTER TABLE users ADD COLUMN sub_organization_name VARCHAR(1200); 23 | 24 | -- Add sub organization id to the passcodes table 25 | ALTER TABLE passcodes 26 | ADD COLUMN sub_organization_name text DEFAULT NULL; 27 | 28 | -- Update varchar limit for in user table for hrn and created_by fields 29 | ALTER TABLE users 30 | ALTER COLUMN hrn TYPE VARCHAR(1300), 31 | ALTER COLUMN created_by TYPE VARCHAR(1300); -------------------------------------------------------------------------------- /src/main/resources/db/migration/V17__add_required_variables_in_poly_templates.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE policy_templates 2 | ADD COLUMN required_variables text[] not null default '{}'; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V18__policy_template_on_create_org_flag.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE policy_templates 2 | ADD COLUMN on_create_org BOOLEAN DEFAULT true NOT NULL; 3 | 4 | ALTER TABLE policy_templates 5 | ALTER COLUMN on_create_org DROP DEFAULT; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V19__add_last_sent_column_in_passcode_table.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE passcodes 2 | ADD COLUMN last_sent TIMESTAMP; 3 | 4 | UPDATE passcodes 5 | SET last_sent = created_at; 6 | 7 | ALTER TABLE passcodes 8 | ALTER COLUMN last_sent SET NOT NULL; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V20__add_link_user_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE link_users ( 2 | id VARCHAR(512) NOT NULL, 3 | leader_user_hrn VARCHAR(1200) NOT NULL, 4 | subordinate_user_hrn VARCHAR(1200) NOT NULL, 5 | created_at timestamp NOT NULL, 6 | updated_at timestamp NOT NULL, 7 | 8 | PRIMARY KEY (id) 9 | ); 10 | 11 | CREATE UNIQUE INDEX leader_subordinate_user_idx ON link_users (leader_user_hrn, subordinate_user_hrn); -------------------------------------------------------------------------------- /src/main/resources/db/migration/V21__change_passcode_purpose_lenght.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE passcodes 2 | ALTER COLUMN purpose TYPE VARCHAR (20); -------------------------------------------------------------------------------- /src/main/resources/db/migration/V2__create_audit_entries_table_and_partitions.sql: -------------------------------------------------------------------------------- 1 | create table audit_entries ( 2 | id uuid DEFAULT gen_random_uuid(), 3 | request_id VARCHAR(200) NOT NULL, 4 | event_time timestamp NOT NULL, 5 | principal VARCHAR(200) NOT NULL, 6 | principal_organization VARCHAR(10) NOT NULL, 7 | resource VARCHAR(200) NOT NULL, 8 | operation VARCHAR(200) NOT NULL, 9 | meta json DEFAULT NULL, 10 | PRIMARY KEY (id, event_time) 11 | ) PARTITION BY RANGE (event_time); 12 | 13 | CREATE INDEX audit_entries_idx_principal_organization_principal_event_time ON audit_entries(principal_organization, principal, event_time); 14 | CREATE INDEX audit_entries_idx_resource_event_time ON audit_entries(resource, event_time); 15 | 16 | CREATE TABLE audit_entries_y2022m02 PARTITION OF audit_entries 17 | FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); 18 | 19 | CREATE TABLE audit_entries_y2022m03 PARTITION OF audit_entries 20 | FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); 21 | 22 | CREATE TABLE audit_entries_y2022m04 PARTITION OF audit_entries 23 | FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); 24 | 25 | CREATE TABLE audit_entries_y2022m05 PARTITION OF audit_entries 26 | FOR VALUES FROM ('2022-05-01') TO ('2022-06-01'); 27 | 28 | CREATE TABLE audit_entries_y2022m06 PARTITION OF audit_entries 29 | FOR VALUES FROM ('2022-06-01') TO ('2022-07-01'); 30 | 31 | CREATE TABLE audit_entries_y2022m07 PARTITION OF audit_entries 32 | FOR VALUES FROM ('2022-07-01') TO ('2022-08-01'); 33 | 34 | CREATE TABLE audit_entries_y2022m08 PARTITION OF audit_entries 35 | FOR VALUES FROM ('2022-08-01') TO ('2022-09-01'); 36 | 37 | CREATE TABLE audit_entries_y2022m09 PARTITION OF audit_entries 38 | FOR VALUES FROM ('2022-09-01') TO ('2022-10-01'); 39 | 40 | CREATE TABLE audit_entries_y2022m10 PARTITION OF audit_entries 41 | FOR VALUES FROM ('2022-10-01') TO ('2022-11-01'); 42 | 43 | CREATE TABLE audit_entries_y2022m11 PARTITION OF audit_entries 44 | FOR VALUES FROM ('2022-11-01') TO ('2022-12-01'); 45 | 46 | CREATE TABLE audit_entries_y2022m12 PARTITION OF audit_entries 47 | FOR VALUES FROM ('2022-12-01') TO ('2023-01-01'); 48 | 49 | CREATE TABLE audit_entries_y2023m01 PARTITION OF audit_entries 50 | FOR VALUES FROM ('2023-01-01') TO ('2023-02-01'); 51 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V3__add_keys_pem_file_to_db.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE master_keys 2 | RENAME private_key TO private_key_der; 3 | 4 | ALTER TABLE master_keys 5 | RENAME public_key TO public_key_der; 6 | 7 | ALTER TABLE master_keys 8 | ADD COLUMN private_key_pem BYTEA, 9 | ADD COLUMN public_key_pem BYTEA; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V4__modify_users_table.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | DROP COLUMN password_hash, 3 | DROP COLUMN phone, 4 | DROP COLUMN login_access, 5 | DROP COLUMN created_by, 6 | ADD COLUMN verified BOOLEAN DEFAULT FALSE, 7 | ADD COLUMN deleted BOOLEAN DEFAULT FALSE; 8 | 9 | DROP INDEX IF EXISTS users_idx_organization_id_name; 10 | CREATE INDEX users_email_organization_index ON users(email, organization_id); 11 | 12 | ALTER TABLE users 13 | DROP CONSTRAINT users_organization_id_fkey, 14 | ADD CONSTRAINT users_organization_id_fkey 15 | FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE; 16 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V5__org_rename_admin_to_root.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE organizations 2 | RENAME COLUMN admin_user_hrn TO root_user_hrn; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V6__create_passcode_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE passcodes ( 2 | id VARCHAR(10) PRIMARY KEY, 3 | valid_until timestamp NOT NULL, 4 | email VARCHAR(50) NOT NULL, 5 | organization_id VARCHAR(10), 6 | purpose VARCHAR(10) NOT NULL, -- RESET, SIGNUP 7 | 8 | created_at timestamp NOT NULL, 9 | 10 | FOREIGN KEY (organization_id) REFERENCES organizations (id) 11 | ); -------------------------------------------------------------------------------- /src/main/resources/db/migration/V7__add_preferred_username.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ADD COLUMN preferred_username VARCHAR(50); -------------------------------------------------------------------------------- /src/main/resources/db/migration/V8__rename_user_policies_to_principal_policies.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_policies RENAME TO principal_policies; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V9__create_username_index_on_users.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX users_idx_username ON users(preferred_username); 2 | -------------------------------------------------------------------------------- /src/main/resources/default_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "host": "localhost", 4 | "port": "4921", 5 | "username": "root", 6 | "password": "password", 7 | "maximum_pool_size": 32, 8 | "minimum_idle": 5, 9 | "is_auto_commit": true, 10 | "transaction_isolation": "TRANSACTION_REPEATABLE_READ" 11 | }, 12 | "newrelic": { 13 | "api_key": "test_key", 14 | "publish_interval": 10 15 | }, 16 | "server": { 17 | "port": 8080, 18 | "connectionGroupSize": 32, 19 | "workerGroupSize": 64, 20 | "callGroupSize": 256, 21 | "requestQueueLimit": 16, 22 | "runningLimit": 10, 23 | "shareWorkGroup": false, 24 | "responseWriteTimeoutSeconds": 10, 25 | "requestReadTimeoutSeconds": 10, 26 | "tcpKeepAlive": false 27 | }, 28 | "app": { 29 | "env": "Development", 30 | "name": "iam", 31 | "stack": "local", 32 | "jwt_token_validity": 300, 33 | "old_key_ttl": 600, 34 | "secret_key": "iam-secret-key", 35 | "sign_key_fetch_interval": 300, 36 | "cache_refresh_interval": 300, 37 | "passcode_validity_seconds": 86400, 38 | "passcode_count_limit": 5, 39 | "resend_invite_wait_time_seconds": 900, 40 | "base_url": "localhost", 41 | "sender_email_address": "", 42 | "sign_up_email_template": "", 43 | "reset_password_email_template": "", 44 | "invite_user_email_template": "", 45 | "link_user_email_template": "", 46 | "unique_users_across_organizations": false 47 | }, 48 | "aws": { 49 | "region": "us-east-1", 50 | "accessKey": "", 51 | "secretKey": "" 52 | }, 53 | "postHook": { 54 | "signup": "http://posthook.com/signup" 55 | }, 56 | "onboardRoutes": { 57 | "signup": "/signup", 58 | "reset": "/organizations/users/resetPassword", 59 | "invite": "/organizations/users/verifyUser", 60 | "linkUser": "/user_links" 61 | }, 62 | "sub_org_config": { 63 | "base_url": "", 64 | "invite_user_email_template": "", 65 | "reset_password_email_template": "", 66 | "link_user_email_template": "", 67 | "onboardRoutes": { 68 | "signup": "/signup", 69 | "reset": "/reset", 70 | "invite": "/invite", 71 | "linkUser": "/user_links" 72 | } 73 | }, 74 | "cognito": { 75 | "id": "", 76 | "name": "", 77 | "metadata": { 78 | "iam_client_id": "" 79 | }, 80 | "identitySource": "AWS_COGNITO" 81 | } 82 | } -------------------------------------------------------------------------------- /src/main/resources/generate_key_pair.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Navigate to work dir 4 | cd /tmp || exit 1 5 | 6 | # Clean up 7 | rm private_key.pem public_key.pem private_key.der public_key.der 8 | 9 | #- Create a ES256 private key pem: 10 | openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem 11 | 12 | #- Generate corresponding public key pem: 13 | openssl ec -in private_key.pem -pubout -out public_key.pem 14 | 15 | #- Generate private_key.der corresponding to private_key.pem 16 | openssl pkcs8 -topk8 -inform PEM -outform DER -in private_key.pem -out private_key.der -nocrypt 17 | 18 | #- Generate public_key.der corresponding to private_key.pem 19 | openssl ec -in private_key.pem -pubout -outform DER -out public_key.der -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %X{call-id} %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | ${LOG_DEST:-build/output/logs}/iam_application.log 11 | 12 | 13 | ${LOG_DEST:-build/output/logs}/iam_application.%d{yyyy-MM-dd}.log 14 | 15 | 16 | ${LOG_MAX_HISTORY:-90} 17 | 3GB 18 | 19 | 20 | 21 | 22 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 23 | 24 | 25 | 26 | 27 | ${LOG_DEST:-build/output/logs}/iam_audit.log 28 | 29 | 30 | ${LOG_DEST:-build/output/logs}/iam_audit.%d{yyyy-MM-dd}.log 31 | 32 | 33 | ${LOG_MAX_HISTORY:-90} 34 | 3GB 35 | 36 | 37 | 38 | 39 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %X{call-id} %-5level %logger{36} | %marker | - %msg%n 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/ExceptionHandlerTest.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server 2 | 3 | import com.google.gson.Gson 4 | import com.hypto.iam.server.helpers.AbstractContainerBaseTest 5 | import com.hypto.iam.server.models.CreateOrganizationRequest 6 | import com.hypto.iam.server.models.RootUser 7 | import com.hypto.iam.server.service.OrganizationsService 8 | import com.hypto.iam.server.service.TokenServiceImpl 9 | import com.hypto.iam.server.utils.IdGenerator 10 | import io.ktor.client.request.header 11 | import io.ktor.client.request.post 12 | import io.ktor.client.request.setBody 13 | import io.ktor.client.statement.bodyAsText 14 | import io.ktor.http.ContentType 15 | import io.ktor.http.HttpHeaders 16 | import io.ktor.http.HttpStatusCode 17 | import io.ktor.server.config.ApplicationConfig 18 | import io.ktor.server.testing.testApplication 19 | import io.mockk.coEvery 20 | import org.junit.jupiter.api.Assertions 21 | import org.junit.jupiter.api.Test 22 | import org.koin.test.inject 23 | import org.koin.test.mock.declareMock 24 | 25 | class ExceptionHandlerTest : AbstractContainerBaseTest() { 26 | private val gson: Gson by inject() 27 | 28 | @Test 29 | fun `StatusPage - Respond to server side errors with custom error message`() { 30 | declareMock { 31 | coEvery { this@declareMock.createOrganization(any(), TokenServiceImpl.ISSUER) } coAnswers { 32 | @Suppress("TooGenericExceptionThrown") 33 | throw RuntimeException() 34 | } 35 | } 36 | 37 | testApplication { 38 | environment { 39 | config = ApplicationConfig("application-custom.conf") 40 | } 41 | val orgName = "test-org" + IdGenerator.randomId() 42 | val preferredUsername = "user" + IdGenerator.randomId() 43 | val name = "test name" 44 | val testEmail = "test-user-email" + IdGenerator.randomId() + "@hypto.in" 45 | val testPhone = "+919626012778" 46 | val testPassword = "testPassword@Hash1" 47 | 48 | val requestBody = 49 | CreateOrganizationRequest( 50 | orgName, 51 | RootUser( 52 | preferredUsername = preferredUsername, 53 | name = name, 54 | password = testPassword, 55 | email = testEmail, 56 | phone = testPhone, 57 | ), 58 | ) 59 | val response = 60 | client.post("/organizations") { 61 | header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) 62 | header("X-Api-Key", rootToken) 63 | setBody(gson.toJson(requestBody)) 64 | } 65 | Assertions.assertEquals("{\"message\":\"Internal Server Error Occurred\"}", response.bodyAsText()) 66 | Assertions.assertEquals(HttpStatusCode.InternalServerError, response.status) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/apis/KeyApiTest.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.apis 2 | 3 | import com.hypto.iam.server.helpers.BaseSingleAppTest 4 | import com.hypto.iam.server.helpers.DataSetupHelperV3.createOrganization 5 | import com.hypto.iam.server.helpers.DataSetupHelperV3.deleteOrganization 6 | import com.hypto.iam.server.models.KeyResponse 7 | import com.hypto.iam.server.models.TokenResponse 8 | import com.hypto.iam.server.service.TokenServiceImpl 9 | import io.jsonwebtoken.Jwts 10 | import io.ktor.client.request.get 11 | import io.ktor.client.request.header 12 | import io.ktor.client.request.post 13 | import io.ktor.client.statement.bodyAsText 14 | import io.ktor.http.ContentType.Application.Json 15 | import io.ktor.http.HttpHeaders 16 | import io.ktor.http.HttpStatusCode 17 | import io.ktor.http.contentType 18 | import io.ktor.test.dispatcher.testSuspend 19 | import org.junit.jupiter.api.Assertions.assertEquals 20 | import org.junit.jupiter.api.Test 21 | import org.junit.jupiter.api.assertDoesNotThrow 22 | import java.security.KeyFactory 23 | import java.security.PublicKey 24 | import java.security.spec.X509EncodedKeySpec 25 | import java.util.Base64 26 | 27 | class KeyApiTest : BaseSingleAppTest() { 28 | @Test 29 | fun `validate token using public key`() { 30 | testSuspend { 31 | val (organizationResponse, _) = testApp.createOrganization() 32 | 33 | val createTokenCall = 34 | testApp.client.post("/organizations/${organizationResponse.organization.id}/token") { 35 | header(HttpHeaders.ContentType, Json.toString()) 36 | header(HttpHeaders.Authorization, "Bearer ${organizationResponse.rootUserToken}") 37 | } 38 | val token = 39 | gson 40 | .fromJson(createTokenCall.bodyAsText(), TokenResponse::class.java).token 41 | 42 | println(token) 43 | 44 | val splitToken: Array = token.split(".").toTypedArray() 45 | val unsignedToken = splitToken[0] + "." + splitToken[1] + "." 46 | val jwt = Jwts.parserBuilder().build().parseClaimsJwt(unsignedToken) 47 | 48 | val kid = jwt.header.getValue(TokenServiceImpl.KEY_ID) as String 49 | 50 | val response = 51 | testApp.client.get( 52 | "/keys/$kid?format=der", 53 | ) { 54 | header(HttpHeaders.Authorization, "Bearer ${organizationResponse.rootUserToken}") 55 | } 56 | assertEquals(HttpStatusCode.OK, response.status) 57 | assertEquals( 58 | Json, 59 | response.contentType(), 60 | ) 61 | val publicKeyResponse = gson.fromJson(response.bodyAsText(), KeyResponse::class.java) 62 | 63 | assertEquals(KeyResponse.Format.der, publicKeyResponse.format) 64 | 65 | val encodedPublicKey = Base64.getDecoder().decode(publicKeyResponse.key) 66 | val keyFactory = KeyFactory.getInstance("EC") 67 | val keySpec = X509EncodedKeySpec(encodedPublicKey) 68 | val publicKey = keyFactory.generatePublic(keySpec) as PublicKey 69 | 70 | assertEquals(kid, publicKeyResponse.kid) 71 | assertDoesNotThrow { 72 | Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token) 73 | } 74 | 75 | testApp.deleteOrganization(organizationResponse.organization.id) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/helpers/AbstractContainerBaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.helpers 2 | 3 | import com.hypto.iam.server.Constants 4 | import com.hypto.iam.server.configs.AppConfig 5 | import com.hypto.iam.server.db.repositories.PasscodeRepo 6 | import com.hypto.iam.server.di.applicationModule 7 | import com.hypto.iam.server.di.controllerModule 8 | import com.hypto.iam.server.di.getKoinInstance 9 | import com.hypto.iam.server.di.repositoryModule 10 | import io.mockk.mockkClass 11 | import okhttp3.OkHttpClient 12 | import org.junit.jupiter.api.BeforeEach 13 | import org.junit.jupiter.api.extension.RegisterExtension 14 | import org.koin.test.junit5.AutoCloseKoinTest 15 | import org.koin.test.junit5.KoinTestExtension 16 | import org.koin.test.junit5.mock.MockProviderExtension 17 | import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient 18 | import software.amazon.awssdk.services.ses.SesClient 19 | 20 | abstract class AbstractContainerBaseTest : AutoCloseKoinTest() { 21 | protected var rootToken: String = "" 22 | protected lateinit var cognitoClient: CognitoIdentityProviderClient 23 | protected lateinit var passcodeRepo: PasscodeRepo 24 | protected lateinit var sesClient: SesClient 25 | protected lateinit var okHttpClient: OkHttpClient 26 | 27 | @JvmField 28 | @RegisterExtension 29 | val koinTestExtension = 30 | KoinTestExtension.create { 31 | modules(repositoryModule, controllerModule, applicationModule) 32 | } 33 | 34 | @JvmField 35 | @RegisterExtension 36 | val koinMockProvider = MockProviderExtension.create { mockkClass(it) } 37 | 38 | @BeforeEach 39 | fun setup() { 40 | rootToken = Constants.SECRET_PREFIX + getKoinInstance().app.secretKey 41 | passcodeRepo = mockPasscodeRepo() 42 | cognitoClient = mockCognitoClient() 43 | sesClient = mockSesClient() 44 | okHttpClient = mockOkHttpClient() 45 | } 46 | 47 | init { 48 | PostgresInit 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/helpers/BaseSingleAppTest.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.helpers 2 | 3 | import com.google.gson.Gson 4 | import io.ktor.server.config.ApplicationConfig 5 | import io.ktor.server.testing.TestApplication 6 | import org.junit.jupiter.api.AfterAll 7 | import org.junit.jupiter.api.BeforeAll 8 | import org.koin.test.inject 9 | 10 | abstract class BaseSingleAppTest : AbstractContainerBaseTest() { 11 | protected val gson: Gson by inject() 12 | 13 | companion object { 14 | lateinit var testApp: TestApplication 15 | 16 | @JvmStatic 17 | @BeforeAll 18 | fun setupTest() { 19 | testApp = 20 | TestApplication { 21 | environment { 22 | config = ApplicationConfig("application-custom.conf") 23 | } 24 | } 25 | } 26 | 27 | @JvmStatic 28 | @AfterAll 29 | fun teardownTest() { 30 | testApp.stop() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/helpers/MockOkHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.helpers 2 | 3 | import io.mockk.coEvery 4 | import io.mockk.mockk 5 | import okhttp3.OkHttpClient 6 | import org.koin.core.qualifier.named 7 | import org.koin.test.KoinTest 8 | import org.koin.test.mock.declareMock 9 | 10 | fun KoinTest.mockOkHttpClient(): OkHttpClient = 11 | declareMock(named("AuthProvider")) { 12 | coEvery { newCall(any()) } returns 13 | mockk { 14 | coEvery { execute() } returns 15 | mockk { 16 | coEvery { isSuccessful } returns true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/helpers/MockPasscodeRepo.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.helpers 2 | 3 | import com.hypto.iam.server.db.repositories.PasscodeRepo 4 | import com.hypto.iam.server.db.tables.records.PasscodesRecord 5 | import com.hypto.iam.server.models.VerifyEmailRequest 6 | import io.mockk.coEvery 7 | import org.koin.test.KoinTest 8 | import org.koin.test.mock.declareMock 9 | 10 | fun KoinTest.mockPasscodeRepo(): PasscodeRepo { 11 | return declareMock { 12 | coEvery { 13 | createPasscode(any()) 14 | } coAnswers { 15 | firstArg() 16 | } 17 | coEvery { 18 | getValidPasscodeCount(any(), any()) 19 | } returns 0 20 | coEvery { 21 | getValidPasscodeCount(any(), any(), any(), any()) 22 | } returns 0 23 | coEvery { 24 | getValidPasscodeById( 25 | any(), 26 | any(), 27 | any(), 28 | ) 29 | } coAnswers { 30 | PasscodesRecord().setId(firstArg()).setPurpose(secondArg().toString()) 31 | .setEmail(thirdArg()) 32 | } 33 | coEvery { deleteByEmailAndPurpose(any(), any()) } returns true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/helpers/MockSes.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.helpers 2 | 3 | import io.mockk.coEvery 4 | import org.koin.test.KoinTest 5 | import org.koin.test.mock.declareMock 6 | import software.amazon.awssdk.services.ses.SesClient 7 | import software.amazon.awssdk.services.ses.model.SendTemplatedEmailRequest 8 | import software.amazon.awssdk.services.ses.model.SendTemplatedEmailResponse 9 | 10 | fun KoinTest.mockSesClient(): SesClient { 11 | return declareMock { 12 | coEvery { 13 | this@declareMock.sendTemplatedEmail(any()) 14 | } coAnswers { 15 | SendTemplatedEmailResponse.builder().messageId("1234-5678-3421").build() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/helpers/PostgresInit.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.helpers 2 | 3 | import org.flywaydb.core.Flyway 4 | import org.flywaydb.core.api.Location 5 | import org.flywaydb.core.api.configuration.ClassicConfiguration 6 | import org.testcontainers.containers.PostgreSQLContainer 7 | 8 | /** 9 | * This object is used to initialize the testcontainers only once for all the tests. 10 | */ 11 | object PostgresInit { 12 | init { 13 | val testContainer = 14 | PostgreSQLContainer("postgres:14.1-alpine") 15 | .withDatabaseName("iam") 16 | .withUsername("root") 17 | .withPassword("password") 18 | 19 | testContainer.start() 20 | 21 | val configuration = ClassicConfiguration() 22 | configuration.setDataSource( 23 | testContainer.jdbcUrl, 24 | testContainer.username, 25 | testContainer.password, 26 | ) 27 | configuration.setLocations(Location("filesystem:src/main/resources/db/migration")) 28 | val flyway = Flyway(configuration) 29 | flyway.migrate() 30 | 31 | System.setProperty("config.override.database.name", "iam") 32 | System.setProperty("config.override.database.user", "root") 33 | System.setProperty("config.override.database.password", "password") 34 | System.setProperty("config.override.database.host", testContainer.host) 35 | System.setProperty( 36 | "config.override.database.port", 37 | testContainer.firstMappedPort.toString(), 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/utils/HrnTest.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.utils 2 | 3 | import org.junit.jupiter.api.Assertions 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Test 6 | 7 | internal class HrnTest { 8 | @Test 9 | fun `Test hrn factory`() { 10 | // Invalid Hrn formats 11 | Assertions.assertThrows(HrnParseException::class.java) { HrnFactory.getHrn("") } 12 | Assertions.assertThrows(HrnParseException::class.java) { HrnFactory.getHrn("hrn:iam:::user1") } 13 | Assertions.assertThrows(HrnParseException::class.java) { HrnFactory.getHrn("hrn:iam:user") } 14 | Assertions.assertThrows(HrnParseException::class.java) { HrnFactory.getHrn("hrn::id1:user") } 15 | Assertions.assertThrows(HrnParseException::class.java) { HrnFactory.getHrn("hrn:id1:user") } 16 | Assertions.assertThrows(HrnParseException::class.java) { HrnFactory.getHrn(":iam:id1:user") } 17 | Assertions.assertThrows(HrnParseException::class.java) { HrnFactory.getHrn("iam:id1:user") } 18 | Assertions.assertThrows(HrnParseException::class.java) { HrnFactory.getHrn("hrn:id1:\$user") } 19 | 20 | val resourceInstanceHrn = HrnFactory.getHrn("hrn:hypto::iam-resource/12345") 21 | assert(resourceInstanceHrn is ResourceHrn) { "Resource instance hrn must be of type ResourceHrn" } 22 | 23 | val userInstanceHrn = HrnFactory.getHrn("hrn:hypto::iam-user/12345") 24 | assert(userInstanceHrn is ResourceHrn) { "User instance hrn must be of type ResourceHrn" } 25 | 26 | val resourceHrn = HrnFactory.getHrn("hrn:hypto::ledger") 27 | assert(resourceHrn is ResourceHrn) { "Global resources are still Resources, so type should be ResourceHrn" } 28 | assertEquals("", (resourceHrn as ResourceHrn).resourceInstance) 29 | 30 | val actionHrn = HrnFactory.getHrn("hrn:hypto::ledger\$addTransaction") 31 | assert(actionHrn is ActionHrn) { "Operations are global, so type should be of GlobalHrn" } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/utils/IdGeneratorTest.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.utils 2 | 3 | import org.junit.jupiter.api.Assertions 4 | import org.junit.jupiter.api.Test 5 | import java.lang.Thread.sleep 6 | 7 | class IdGeneratorTest { 8 | private val subject = IdGenerator 9 | 10 | @Test 11 | fun `Test length based ID generation`() { 12 | Assertions.assertEquals(10, subject.randomId().length) // Default length 13 | Assertions.assertEquals(3, subject.randomId(length = 3).length) 14 | Assertions.assertEquals(12, subject.randomId(length = 12).length) 15 | Assertions.assertEquals(55, subject.randomId(length = 55).length) 16 | Assertions.assertEquals(100, subject.randomId(length = 100).length) 17 | } 18 | 19 | @Test 20 | fun `Test Charset based ID generation`() { 21 | val rand1 = subject.randomId(charset = IdGenerator.Charset.UPPERCASE_ALPHABETS) 22 | Assertions.assertEquals(rand1.uppercase(), rand1) 23 | Assertions.assertTrue(rand1.all { it.isLetter() }) 24 | 25 | val rand2 = subject.randomId(charset = IdGenerator.Charset.LOWERCASE_ALPHABETS) 26 | Assertions.assertEquals(rand2.lowercase(), rand2) 27 | Assertions.assertTrue(rand2.all { it.isLetter() }) 28 | 29 | val rand3 = subject.randomId(charset = IdGenerator.Charset.NUMERIC) 30 | Assertions.assertTrue(rand3.all { it.isDigit() }) 31 | 32 | val rand4 = subject.randomId(charset = IdGenerator.Charset.LOWER_ALPHANUMERIC) 33 | Assertions.assertTrue(rand4.all { it.isDigit() || it.isLowerCase() }) 34 | 35 | val rand5 = subject.randomId(charset = IdGenerator.Charset.UPPER_ALPHANUMERIC) 36 | Assertions.assertTrue(rand5.all { it.isDigit() || it.isUpperCase() }) 37 | 38 | val rand6 = subject.randomId(charset = IdGenerator.Charset.ALPHABETS) 39 | Assertions.assertTrue(rand6.all { it.isLetter() }) 40 | 41 | val rand7 = subject.randomId(charset = IdGenerator.Charset.ALPHANUMERIC) 42 | Assertions.assertTrue(rand7.all { it.isDigit() || it.isLetter() }) 43 | } 44 | 45 | @Test 46 | fun `Test numberToId`() { 47 | for (charSet in IdGenerator.Charset.values()) { 48 | for (i in 0 until charSet.seed.length) { 49 | Assertions.assertEquals( 50 | subject.numberToId(i.toLong(), charSet), 51 | charSet.seed[i].toString(), 52 | ) 53 | } 54 | } 55 | 56 | val number = 1645204287L 57 | 58 | Assertions.assertEquals(subject.numberToId(number, IdGenerator.Charset.NUMERIC), "1645204287") 59 | Assertions.assertEquals(subject.numberToId(number, IdGenerator.Charset.ALPHABETS), "ERAhbz") 60 | Assertions.assertEquals(subject.numberToId(number, IdGenerator.Charset.ALPHANUMERIC), "BxVGxB") 61 | Assertions.assertEquals(subject.numberToId(number, IdGenerator.Charset.UPPERCASE_ALPHABETS), "FIMFEDZ") 62 | Assertions.assertEquals(subject.numberToId(number, IdGenerator.Charset.LOWERCASE_ALPHABETS), "fimfedz") 63 | Assertions.assertEquals(subject.numberToId(number, IdGenerator.Charset.UPPER_ALPHANUMERIC), "1HSP1D") 64 | Assertions.assertEquals(subject.numberToId(number, IdGenerator.Charset.LOWER_ALPHANUMERIC), "1hsp1d") 65 | } 66 | 67 | @Test 68 | fun `Test timeBasedRandomId`() { 69 | Assertions.assertThrows(IllegalArgumentException::class.java) { subject.timeBasedRandomId(length = 5) } 70 | 71 | Assertions.assertEquals(subject.timeBasedRandomId(10).length, 10) 72 | 73 | val id1 = subject.timeBasedRandomId() 74 | sleep(1) 75 | val id2 = subject.timeBasedRandomId() 76 | Assertions.assertTrue(id2 > id1) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/kotlin/com/hypto/iam/server/validators/RequestModelValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package com.hypto.iam.server.validators 2 | 3 | import com.hypto.iam.server.models.CreateCredentialRequest 4 | import com.hypto.iam.server.models.UpdateCredentialRequest 5 | import org.junit.jupiter.api.Assertions 6 | import org.junit.jupiter.api.Test 7 | import java.time.LocalDateTime 8 | import java.time.format.DateTimeFormatter 9 | 10 | internal class RequestModelValidatorTest { 11 | // CreateCredentialRequest validations 12 | @Test 13 | fun `CreateCredentialRequest - valid - without validity `() { 14 | val req = CreateCredentialRequest() 15 | Assertions.assertInstanceOf(CreateCredentialRequest::class.java, req.validate()) 16 | } 17 | 18 | @Test 19 | fun `CreateCredentialRequest - valid - with validity `() { 20 | val req = CreateCredentialRequest(LocalDateTime.MAX.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) 21 | Assertions.assertInstanceOf(CreateCredentialRequest::class.java, req.validate()) 22 | } 23 | 24 | @Test 25 | fun `CreateCredentialRequest - invalid - validity in the past`() { 26 | val req = CreateCredentialRequest(LocalDateTime.MIN.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) 27 | Assertions.assertThrows(IllegalArgumentException::class.java) { 28 | req.validate() 29 | } 30 | } 31 | 32 | @Test 33 | fun `CreateCredentialRequest - invalid - invalid validity string format`() { 34 | val req = CreateCredentialRequest("not a valid datetime") 35 | Assertions.assertThrows(IllegalArgumentException::class.java) { 36 | req.validate() 37 | } 38 | } 39 | 40 | // UpdateCredentialRequest validations 41 | 42 | @Test 43 | fun `UpdateCredentialRequest - valid - with only status `() { 44 | val req = UpdateCredentialRequest(status = UpdateCredentialRequest.Status.active) 45 | Assertions.assertInstanceOf(UpdateCredentialRequest::class.java, req.validate()) 46 | } 47 | 48 | @Test 49 | fun `UpdateCredentialRequest - valid - with only validity `() { 50 | val req = UpdateCredentialRequest(LocalDateTime.MAX.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) 51 | Assertions.assertInstanceOf(UpdateCredentialRequest::class.java, req.validate()) 52 | } 53 | 54 | @Test 55 | fun `UpdateCredentialRequest - valid - with both status and validity `() { 56 | val req = 57 | UpdateCredentialRequest( 58 | LocalDateTime.MAX.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), 59 | UpdateCredentialRequest.Status.inactive, 60 | ) 61 | Assertions.assertInstanceOf(UpdateCredentialRequest::class.java, req.validate()) 62 | } 63 | 64 | @Test 65 | fun `UpdateCredentialRequest - invalid - without status and validity `() { 66 | val req = UpdateCredentialRequest() 67 | Assertions.assertThrows(IllegalArgumentException::class.java) { 68 | Assertions.assertInstanceOf(UpdateCredentialRequest::class.java, req.validate()) 69 | } 70 | } 71 | 72 | @Test 73 | fun `UpdateCredentialRequest - invalid - validity in the past`() { 74 | val req = UpdateCredentialRequest(LocalDateTime.MIN.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) 75 | Assertions.assertThrows(IllegalArgumentException::class.java) { 76 | req.validate() 77 | } 78 | } 79 | 80 | @Test 81 | fun `UpdateCredentialRequest - invalid - invalid validity string format`() { 82 | val req = UpdateCredentialRequest("not a valid datetime") 83 | Assertions.assertThrows(IllegalArgumentException::class.java) { 84 | req.validate() 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jooq/impl/ResultImplTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * We need this package because ResultImpl is package-private and cannot be directly used in tests. 3 | */ 4 | package org.jooq.impl 5 | 6 | import io.mockk.mockk 7 | import org.jooq.Field 8 | import org.jooq.Record 9 | import org.jooq.Result 10 | 11 | fun getResultImpl(records: List): Result { 12 | val result = ResultImpl(mockk(), mockk>()) 13 | result.addAll(records) 14 | return result 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/application-custom.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | application { 3 | modules = [ com.hypto.iam.server.ApplicationKt.handleRequest ] 4 | } 5 | } -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | ${TEST_LOG_DEST:-build/output/logs}/iam_application.log 9 | 10 | 11 | ${TEST_LOG_DEST:-build/test/output/logs}/iam_application.%d{yyyy-MM-dd}.log 12 | 13 | 14 | ${TEST_LOG_MAX_HISTORY:-1} 15 | 100MB 16 | 17 | 18 | 19 | 20 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | --------------------------------------------------------------------------------