diff --git a/backend/src/strategies/github-token/github-token.service.ts b/backend/src/strategies/github-token/github-token.service.ts index 4f84bbd3..aced6f57 100644 --- a/backend/src/strategies/github-token/github-token.service.ts +++ b/backend/src/strategies/github-token/github-token.service.ts @@ -64,7 +64,7 @@ export class GithubTokenStrategyService extends Strategy { * - graphql-url: The URL of the github graphql endpoint. * If imsTemplatedFieldsFilter not given, defaults to "https://api.github.com/graphql" * - * @param instanceConfig The instance config for a github strategy instance to check + * @param instanceConfig The instance config for a github-token strategy instance to check * @returns The extended config (with default parameters for the global github) if check successful */ protected override checkAndExtendInstanceConfig(instanceConfig: object): object { @@ -112,11 +112,6 @@ export class GithubTokenStrategyService extends Strategy { override getCensoredInstanceConfig(instance: StrategyInstance): object { return { imsTemplatedFieldsFilter: instance.instanceConfig["imsTemplatedFieldsFilter"], - authorizationUrl: instance.instanceConfig["authorizationUrl"], - tokenUrl: instance.instanceConfig["tokenUrl"], - userProfileUrl: instance.instanceConfig["userProfileUrl"], - clientId: instance.instanceConfig["clientId"], - clientSecret: "**********", }; } diff --git a/backend/src/strategies/jira-token-cloud/jira-token-cloud.service.ts b/backend/src/strategies/jira-token-cloud/jira-token-cloud.service.ts new file mode 100644 index 00000000..68f3bc2b --- /dev/null +++ b/backend/src/strategies/jira-token-cloud/jira-token-cloud.service.ts @@ -0,0 +1,203 @@ +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 JiraTokenCloudStrategyService extends Strategy { + constructor( + strategiesService: StrategiesService, + strategyInstanceService: StrategyInstanceService, + private readonly loginDataService: UserLoginDataService, + ) { + super("jira-token-cloud", strategyInstanceService, strategiesService, false, true, false, false, false); + } + + override get instanceConfigSchema(): Record { + return { + imsTemplatedFieldsFilter: { + properties: { + "root-url": { type: "string" }, + }, + }, + }; + } + + override get acceptsVariables(): StrategyVariable[] { + return [ + { + name: "email", + displayName: "Email", + type: "string", + }, + { + 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", + }, + { + name: "email", + displayName: "Email (if changed)", + type: "string", + nullable: true, + }, + ], + }, + ]; + } + + /** + * 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-cloud 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?.email; + } + + override getCensoredInstanceConfig(instance: StrategyInstance): object { + return { + imsTemplatedFieldsFilter: instance.instanceConfig["imsTemplatedFieldsFilter"], + }; + } + + private async getUserData( + token: string, + email: 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: `Basic ${btoa(`${email}:${token}`)}`, + Accept: "application/json", + }, + }, + ); + + if (!response.ok) { + return null; + } + + const userData = await response.json(); + + return { + jira_id: userData.accountId, + username: "", + displayName: userData.displayName, + email, + }; + } + + override async performAuth( + strategyInstance: StrategyInstance, + state: (AuthStateServerData & OAuthAuthorizeServerState) | undefined, + req: any, + res: any, + ): Promise { + const token = req.query["token"]; + const email = req.query["email"]; + + const userLoginData = await this.getUserData(token, email, 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 email = data["email"] || loginData.data["email"]; + const userLoginData = await this.getUserData(apiToken, email, 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; + loginData.data["email"] = email; + this.loginDataService.save(loginData); + } else { + throw new HttpException("Unknown action", HttpStatus.BAD_REQUEST); + } + } +} diff --git a/backend/src/strategies/jira/jira.service.ts b/backend/src/strategies/jira/jira.service.ts index 663568db..bebc3791 100644 --- a/backend/src/strategies/jira/jira.service.ts +++ b/backend/src/strategies/jira/jira.service.ts @@ -168,7 +168,7 @@ export class JiraStrategyService extends StrategyUsingPassport { } else { this.loggerJira.log("Refreshed token valid"); firstLogin = await this.activeLoginService.save(firstLogin); - return { token: firstLogin?.data["accessToken"] ?? null, cloudIds: cloudIds }; + return { token: firstLogin?.data["accessToken"] ?? null, cloudIds: cloudIds, type: "OAUTH" }; } } else { this.loggerJira.log("Non valid cloud IDs, and no refresh token token"); @@ -271,6 +271,10 @@ export class JiraStrategyService extends StrategyUsingPassport { return new passportJira(config, this.passportUserCallback.bind(this, strategyInstance)); } + override async getLoginDataDescription(loginData: UserLoginData): Promise { + return loginData.data?.email; + } + override getCensoredInstanceConfig(instance: StrategyInstance): object { return { imsTemplatedFieldsFilter: instance.instanceConfig["imsTemplatedFieldsFilter"], diff --git a/backend/src/strategies/strategies.module.ts b/backend/src/strategies/strategies.module.ts index 6c9dd9e2..e7c0d7cc 100644 --- a/backend/src/strategies/strategies.module.ts +++ b/backend/src/strategies/strategies.module.ts @@ -8,6 +8,7 @@ import { BackendServicesModule } from "src/backend-services/backend-services.mod 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"; @Module({ imports: [ @@ -36,6 +37,7 @@ import { GithubTokenStrategyService } from "./github-token/github-token.service" GithubStrategyService, JiraStrategyService, GithubTokenStrategyService, + JiraTokenCloudStrategyService, { provide: "StateJwtService", useExisting: JwtService }, StrategiesMiddleware, ],