diff --git a/backend/src/api-internal/dto/self-register-user-input.dto.ts b/backend/src/api-internal/dto/self-register-user-input.dto.ts index 8e74fb29..620197a2 100644 --- a/backend/src/api-internal/dto/self-register-user-input.dto.ts +++ b/backend/src/api-internal/dto/self-register-user-input.dto.ts @@ -37,4 +37,4 @@ export class SelfRegisterUserInput extends BaseUserInput { RegistrationTokenInput.check(input); return input; } -} \ No newline at end of file +} diff --git a/backend/src/api-login/auth/dto/create-auth-client.dto.ts b/backend/src/api-login/auth/dto/create-auth-client.dto.ts index 03d4e2ee..42591905 100644 --- a/backend/src/api-login/auth/dto/create-auth-client.dto.ts +++ b/backend/src/api-login/auth/dto/create-auth-client.dto.ts @@ -11,11 +11,11 @@ export class CreateAuthClientInput extends UpdateAuthClientInput { /** * Checks whether the input is a valid `CreateAuthClientInput` - * + * * Needed: * - Must be valid {@link UpdateAuthClientInput} * - Must have a non empty name - * + * * @param input The input object to check * @returns The original input object */ diff --git a/backend/src/api-login/auth/dto/update-auth-client.dto.ts b/backend/src/api-login/auth/dto/update-auth-client.dto.ts index 1047f01b..1c024413 100644 --- a/backend/src/api-login/auth/dto/update-auth-client.dto.ts +++ b/backend/src/api-login/auth/dto/update-auth-client.dto.ts @@ -100,10 +100,7 @@ export class UpdateAuthClientInput { } for (const scope of input.validScopes) { if (scope !== TokenScope.BACKEND) { - throw new HttpException( - `Only ${TokenScope.BACKEND} is a valid scopes`, - HttpStatus.BAD_REQUEST, - ); + throw new HttpException(`Only ${TokenScope.BACKEND} is a valid scopes`, HttpStatus.BAD_REQUEST); } } if (input.clientCredentialFlowUser != undefined) { diff --git a/backend/src/api-oauth/dto/oauth-token-response.dto.ts b/backend/src/api-oauth/dto/oauth-token-response.dto.ts index 4541074d..ffafa082 100644 --- a/backend/src/api-oauth/dto/oauth-token-response.dto.ts +++ b/backend/src/api-oauth/dto/oauth-token-response.dto.ts @@ -4,4 +4,4 @@ export class OAuthTokenResponseDto { expires_in: number; refresh_token?: string; scope: string; -} \ No newline at end of file +} diff --git a/backend/src/api-oauth/oauth-token.controller.ts b/backend/src/api-oauth/oauth-token.controller.ts index 9167a651..20a3c06c 100644 --- a/backend/src/api-oauth/oauth-token.controller.ts +++ b/backend/src/api-oauth/oauth-token.controller.ts @@ -6,7 +6,6 @@ import { OAuthTokenResponseDto } from "./dto/oauth-token-response.dto"; @Controller() @ApiTags(OpenApiTag.OAUTH_API) export class OAuthTokenController { - @Post("token") @ApiOperation({ summary: "Token OAuth Endpoint" }) @ApiOkResponse({ type: OAuthTokenResponseDto }) diff --git a/backend/src/database-migrations/1721847625737-migration.ts b/backend/src/database-migrations/1721847625737-migration.ts index 71ac5cfd..a4dfa438 100644 --- a/backend/src/database-migrations/1721847625737-migration.ts +++ b/backend/src/database-migrations/1721847625737-migration.ts @@ -1,18 +1,22 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class Migration1721847625737 implements MigrationInterface { - name = 'Migration1721847625737' + name = "Migration1721847625737"; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "active_login" DROP CONSTRAINT "FK_e6358a5261dc7e791810e04394c"`); await queryRunner.query(`ALTER TABLE "active_login" DROP COLUMN "createdByClientId"`); - await queryRunner.query(`ALTER TABLE "active_login" RENAME COLUMN "usedStrategyInstnceId" TO "usedStrategyInstanceId"`); + await queryRunner.query( + `ALTER TABLE "active_login" RENAME COLUMN "usedStrategyInstnceId" TO "usedStrategyInstanceId"`, + ); await queryRunner.query(`ALTER TABLE "strategy_instance" ALTER COLUMN "name" SET NOT NULL`); await queryRunner.query(`ALTER TABLE "auth_client" ALTER COLUMN "name" SET NOT NULL`); await queryRunner.query(`ALTER TABLE "auth_client" ADD "validScopes" json NOT NULL DEFAULT '[]'`); await queryRunner.query(`ALTER TABLE "auth_client" ALTER COLUMN "validScopes" DROP DEFAULT`); await queryRunner.query(`ALTER TABLE "auth_client" ADD COLUMN "clientCredentialFlowUserId" uuid NULL`); - await queryRunner.query(`ALTER TABLE "auth_client" ADD CONSTRAINT "FK_42cc6dd6f24948b39263c943b2a" FOREIGN KEY ("clientCredentialFlowUserId") REFERENCES "login_user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query( + `ALTER TABLE "auth_client" ADD CONSTRAINT "FK_42cc6dd6f24948b39263c943b2a" FOREIGN KEY ("clientCredentialFlowUserId") REFERENCES "login_user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); } public async down(queryRunner: QueryRunner): Promise { @@ -22,8 +26,11 @@ export class Migration1721847625737 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "auth_client" ALTER COLUMN "name" DROP NOT NULL`); await queryRunner.query(`ALTER TABLE "strategy_instance" ALTER COLUMN "name" DROP NOT NULL`); await queryRunner.query(`ALTER TABLE "active_login" ADD "createdByClientId" uuid`); - await queryRunner.query(`ALTER TABLE "active_login" RENAME COLUMN "usedStrategyInstanceId" TO "usedStrategyInstnceId"`); - await queryRunner.query(`ALTER TABLE "active_login" ADD CONSTRAINT "FK_e6358a5261dc7e791810e04394c" FOREIGN KEY ("createdByClientId") REFERENCES "auth_client"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query( + `ALTER TABLE "active_login" RENAME COLUMN "usedStrategyInstanceId" TO "usedStrategyInstnceId"`, + ); + await queryRunner.query( + `ALTER TABLE "active_login" ADD CONSTRAINT "FK_e6358a5261dc7e791810e04394c" FOREIGN KEY ("createdByClientId") REFERENCES "auth_client"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); } - } diff --git a/backend/src/strategies/jira-token-datacenter/jira-token-datacenter.service.ts b/backend/src/strategies/jira-token-datacenter/jira-token-datacenter.service.ts new file mode 100644 index 00000000..0ad93ca3 --- /dev/null +++ b/backend/src/strategies/jira-token-datacenter/jira-token-datacenter.service.ts @@ -0,0 +1,188 @@ +import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; +import { PerformAuthResult, Strategy, StrategyUpdateAction, StrategyVariable } from "../Strategy"; +import { OAuthAuthorizeServerState } from "src/api-oauth/OAuthAuthorizeServerState"; +import { StrategyInstance } from "src/model/postgres/StrategyInstance.entity"; +import { AuthStateServerData } from "../AuthResult"; +import { Schema } from "jtd"; +import { StrategiesService } from "src/model/services/strategies.service"; +import { StrategyInstanceService } from "src/model/services/strategy-instance.service"; +import { UserLoginData } from "src/model/postgres/UserLoginData.entity"; +import { UserLoginDataService } from "src/model/services/user-login-data.service"; + +@Injectable() +export class JiraTokenDatacenterStrategyService extends Strategy { + constructor( + strategiesService: StrategiesService, + strategyInstanceService: StrategyInstanceService, + private readonly loginDataService: UserLoginDataService, + ) { + super("jira-token-datacenter", strategyInstanceService, strategiesService, false, true, false, false, false); + } + + override get instanceConfigSchema(): Record { + return { + imsTemplatedFieldsFilter: { + properties: { + "root-url": { type: "string" }, + }, + }, + }; + } + + override get acceptsVariables(): StrategyVariable[] { + return [ + { + name: "token", + displayName: "API token", + type: "password", + }, + ]; + } + + override get updateActions(): StrategyUpdateAction[] { + return [ + { + name: "update-token", + displayName: "Update API token", + variables: [ + { + name: "token", + displayName: "API token", + type: "password", + }, + ], + }, + ]; + } + + /** + * Chechs the given config is valid for a jira + * + * Needed parameters + * - imsTemplatedFieldsFilter containing: + * - root-url: The URL of the jira root endpoint, must be provided. + * + * @param instanceConfig The instance config for a jira-token-datacenter strategy instance to check + * @returns The extended config if check successful + */ + protected override checkAndExtendInstanceConfig(instanceConfig: object): object { + const resultingConfig = instanceConfig; + + if (resultingConfig["imsTemplatedFieldsFilter"]) { + const rootUrl = resultingConfig["imsTemplatedFieldsFilter"]["root-url"]; + if (!rootUrl) { + throw new Error("At least Jira URL must be given in imsTemplatedFieldsFilter"); + } + } else { + throw new Error("At least imsTemplatedFieldsFilter must be given"); + } + + return super.checkAndExtendInstanceConfig(instanceConfig); + } + + override async getSyncDataForLoginData( + loginData: UserLoginData, + ): Promise<{ token: string | null; [key: string]: any }> { + return { token: loginData.data["apiToken"] ?? null, type: "PAT" }; + } + + override getImsUserTemplatedValuesForLoginData(loginData: UserLoginData): object { + return { + jira_id: loginData.data["jira_id"], + username: loginData.data["username"], + displayName: loginData.data["displayName"], + email: loginData.data["email"], + }; + } + + override getLoginDataDataForImsUserTemplatedFields(imsUser: object): object | Promise { + return { + jira_id: imsUser["jira_id"], + }; + } + + override async getLoginDataDescription(loginData: UserLoginData): Promise { + return loginData.data?.username || loginData.data?.email; + } + + override getCensoredInstanceConfig(instance: StrategyInstance): object { + return { + imsTemplatedFieldsFilter: instance.instanceConfig["imsTemplatedFieldsFilter"], + }; + } + + private async getUserData( + token: string, + strategyInstance: StrategyInstance, + ): Promise<{ + jira_id: string; + username: string; + displayName: string; + email?: string; + } | null> { + const response = await fetch( + new URL("/rest/api/2/myself", strategyInstance.instanceConfig["imsTemplatedFieldsFilter"]["root-url"]), + { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + }, + ); + + if (!response.ok) { + return null; + } + + const userData = await response.json(); + + return { + jira_id: userData.key, + username: userData.name, + displayName: userData.displayName, + email: userData.emailAddress, + }; + } + + override async performAuth( + strategyInstance: StrategyInstance, + state: (AuthStateServerData & OAuthAuthorizeServerState) | undefined, + req: any, + res: any, + ): Promise { + const token = req.query["token"]; + + const userLoginData = await this.getUserData(token, strategyInstance); + if (userLoginData == null) { + return { result: null, returnedState: {}, info: { message: "Token invalid" } }; + } + + return { + result: { + dataActiveLogin: {}, + dataUserLoginData: userLoginData, + mayRegister: true, + }, + returnedState: {}, + info: {}, + }; + } + + override async handleAction(loginData: UserLoginData, name: string, data: Record): Promise { + if (name === "update-token") { + const apiToken = data["token"]; + const userLoginData = await this.getUserData(apiToken, await loginData.strategyInstance); + if (userLoginData == null) { + throw new HttpException("Token invalid", HttpStatus.BAD_REQUEST); + } + if (loginData.data["jira_id"] !== userLoginData.jira_id) { + throw new HttpException("Token does not match the user", HttpStatus.BAD_REQUEST); + } + loginData.data["apiToken"] = apiToken; + this.loginDataService.save(loginData); + } else { + throw new HttpException("Unknown action", HttpStatus.BAD_REQUEST); + } + } +} diff --git a/backend/src/strategies/strategies.module.ts b/backend/src/strategies/strategies.module.ts index e7c0d7cc..5ca7bfd8 100644 --- a/backend/src/strategies/strategies.module.ts +++ b/backend/src/strategies/strategies.module.ts @@ -9,6 +9,7 @@ import { GithubStrategyService } from "./github/github.service"; import { JiraStrategyService } from "./jira/jira.service"; import { GithubTokenStrategyService } from "./github-token/github-token.service"; import { JiraTokenCloudStrategyService } from "./jira-token-cloud/jira-token-cloud.service"; +import { JiraTokenDatacenterStrategyService } from "./jira-token-datacenter/jira-token-datacenter.service"; @Module({ imports: [ @@ -38,6 +39,7 @@ import { JiraTokenCloudStrategyService } from "./jira-token-cloud/jira-token-clo JiraStrategyService, GithubTokenStrategyService, JiraTokenCloudStrategyService, + JiraTokenDatacenterStrategyService, { provide: "StateJwtService", useExisting: JwtService }, StrategiesMiddleware, ],