microsoft/typespec
Publicmirrored from https://github.com/microsoft/typespecAvailable
packages/http/src/auth.ts
163lines · modecode
| 1 | import { Operation, Program } from "@typespec/compiler"; |
| 2 | import { deepClone, deepEquals } from "@typespec/compiler/utils"; |
| 3 | import { getAuthentication } from "./decorators.js"; |
| 4 | import { |
| 5 | Authentication, |
| 6 | AuthenticationOptionReference, |
| 7 | AuthenticationReference, |
| 8 | HttpAuth, |
| 9 | HttpAuthRef, |
| 10 | HttpService, |
| 11 | HttpServiceAuthentication, |
| 12 | OAuth2Flow, |
| 13 | OAuth2Scope, |
| 14 | Oauth2Auth, |
| 15 | } from "./types.js"; |
| 16 | |
| 17 | /** |
| 18 | * Resolve the authentication for a given operation. |
| 19 | * @param program Program |
| 20 | * @param operation Operation |
| 21 | * @returns Authentication provided on the operation or containing interface or namespace. |
| 22 | */ |
| 23 | export function getAuthenticationForOperation( |
| 24 | program: Program, |
| 25 | operation: Operation |
| 26 | ): Authentication | undefined { |
| 27 | const operationAuth = getAuthentication(program, operation); |
| 28 | if (operationAuth) { |
| 29 | return operationAuth; |
| 30 | } |
| 31 | if (operation.interface !== undefined) { |
| 32 | const interfaceAuth = getAuthentication(program, operation.interface); |
| 33 | if (interfaceAuth) { |
| 34 | return interfaceAuth; |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | let namespace = operation.namespace; |
| 39 | |
| 40 | while (namespace) { |
| 41 | const namespaceAuth = getAuthentication(program, namespace); |
| 42 | if (namespaceAuth) { |
| 43 | return namespaceAuth; |
| 44 | } |
| 45 | namespace = namespace.namespace; |
| 46 | } |
| 47 | return undefined; |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * Compute the authentication for a given service. |
| 52 | * @param service Http Service |
| 53 | * @returns The normalized authentication for a service. |
| 54 | */ |
| 55 | export function resolveAuthentication(service: HttpService): HttpServiceAuthentication { |
| 56 | let schemes: Record<string, HttpAuth> = {}; |
| 57 | let defaultAuth: AuthenticationReference = { options: [] }; |
| 58 | const operationsAuth: Map<Operation, AuthenticationReference> = new Map(); |
| 59 | |
| 60 | if (service.authentication) { |
| 61 | const { newServiceSchemes, authOptions } = gatherAuth(service.authentication, {}); |
| 62 | schemes = newServiceSchemes; |
| 63 | defaultAuth = authOptions; |
| 64 | } |
| 65 | |
| 66 | for (const op of service.operations) { |
| 67 | if (op.authentication) { |
| 68 | const { newServiceSchemes, authOptions } = gatherAuth(op.authentication, schemes); |
| 69 | schemes = newServiceSchemes; |
| 70 | operationsAuth.set(op.operation, authOptions); |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | return { schemes: Object.values(schemes), defaultAuth, operationsAuth }; |
| 75 | } |
| 76 | |
| 77 | function gatherAuth( |
| 78 | authentication: Authentication, |
| 79 | serviceSchemes: Record<string, HttpAuth> |
| 80 | ): { |
| 81 | newServiceSchemes: Record<string, HttpAuth>; |
| 82 | authOptions: AuthenticationReference; |
| 83 | } { |
| 84 | const newServiceSchemes: Record<string, HttpAuth> = serviceSchemes; |
| 85 | const authOptions: AuthenticationReference = { options: [] }; |
| 86 | for (const option of authentication.options) { |
| 87 | const authOption: AuthenticationOptionReference = { all: [] }; |
| 88 | for (const optionScheme of option.schemes) { |
| 89 | const serviceScheme = serviceSchemes[optionScheme.id]; |
| 90 | let newServiceScheme = optionScheme; |
| 91 | if (serviceScheme) { |
| 92 | // If we've seen a different scheme by this id, |
| 93 | // Make sure to not overwrite it |
| 94 | if (!authsAreEqual(serviceScheme, optionScheme)) { |
| 95 | while (serviceSchemes[newServiceScheme.id]) { |
| 96 | newServiceScheme.id = newServiceScheme.id + "_"; |
| 97 | } |
| 98 | } |
| 99 | // Merging scopes when encountering the same Oauth2 scheme |
| 100 | else if (serviceScheme.type === "oauth2" && optionScheme.type === "oauth2") { |
| 101 | const x = mergeOAuthScopes(serviceScheme, optionScheme); |
| 102 | newServiceScheme = x; |
| 103 | } |
| 104 | } |
| 105 | const httpAuthRef = makeHttpAuthRef(optionScheme, newServiceScheme); |
| 106 | newServiceSchemes[newServiceScheme.id] = newServiceScheme; |
| 107 | authOption.all.push(httpAuthRef); |
| 108 | } |
| 109 | authOptions.options.push(authOption); |
| 110 | } |
| 111 | return { newServiceSchemes, authOptions }; |
| 112 | } |
| 113 | |
| 114 | function makeHttpAuthRef(local: HttpAuth, reference: HttpAuth): HttpAuthRef { |
| 115 | if (reference.type === "oauth2" && local.type === "oauth2") { |
| 116 | const scopes: string[] = []; |
| 117 | for (const flow of local.flows) { |
| 118 | scopes.push(...flow.scopes.map((x) => x.value)); |
| 119 | } |
| 120 | return { kind: "oauth2", auth: reference, scopes: scopes }; |
| 121 | } else if (reference.type === "noAuth") { |
| 122 | return { kind: "noAuth", auth: reference }; |
| 123 | } else { |
| 124 | return { kind: "any", auth: reference }; |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | function mergeOAuthScopes<Flows extends OAuth2Flow[]>( |
| 129 | scheme1: Oauth2Auth<Flows>, |
| 130 | scheme2: Oauth2Auth<Flows> |
| 131 | ): Oauth2Auth<Flows> { |
| 132 | const flows = deepClone(scheme1.flows); |
| 133 | flows.forEach((flow1, i) => { |
| 134 | const flow2 = scheme2.flows[i]; |
| 135 | const scopes = Array.from(new Set(flow1.scopes.concat(flow2.scopes))); |
| 136 | flows[i].scopes = scopes; |
| 137 | }); |
| 138 | return { |
| 139 | ...scheme1, |
| 140 | flows, |
| 141 | }; |
| 142 | } |
| 143 | |
| 144 | function setOauth2Scopes<Flows extends OAuth2Flow[]>( |
| 145 | scheme: Oauth2Auth<Flows>, |
| 146 | scopes: OAuth2Scope[] |
| 147 | ): Oauth2Auth<Flows> { |
| 148 | const flows: Flows = deepClone(scheme.flows); |
| 149 | flows.forEach((flow) => { |
| 150 | flow.scopes = scopes; |
| 151 | }); |
| 152 | return { |
| 153 | ...scheme, |
| 154 | flows, |
| 155 | }; |
| 156 | } |
| 157 | |
| 158 | function authsAreEqual(scheme1: HttpAuth, scheme2: HttpAuth): boolean { |
| 159 | if (scheme1.type === "oauth2" && scheme2.type === "oauth2") { |
| 160 | return deepEquals(setOauth2Scopes(scheme1, []), setOauth2Scopes(scheme2, [])); |
| 161 | } |
| 162 | return deepEquals(scheme1, scheme2); |
| 163 | } |
| 164 | |