Skip to content

Commit

Permalink
Operation level authentication and scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
susliko committed Feb 12, 2024
1 parent 02cd6f4 commit 4cd41e2
Show file tree
Hide file tree
Showing 21 changed files with 571 additions and 67 deletions.
9 changes: 9 additions & 0 deletions docs/libraries/http/reference/data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ The URL of the requested resource has been changed permanently. The new URL is g
model TypeSpec.Http.MovedResponse
```

### `NoAuth` {#TypeSpec.Http.NoAuth}

This authentication option signifies that API is not secured at all.
It might be useful when overriding authentication on interface of operation level.

```typespec
model TypeSpec.Http.NoAuth
```

### `NoContentResponse` {#TypeSpec.Http.NoContentResponse}

There is no content to send for this request, but the headers may be useful.
Expand Down
4 changes: 2 additions & 2 deletions docs/libraries/http/reference/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,15 +402,15 @@ op create(): {@statusCode: 201 | 202}

### `@useAuth` {#@TypeSpec.Http.useAuth}

Specify this service authentication. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.
Specify authentication for a whole service or specific methods. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.

```typespec
@TypeSpec.Http.useAuth(auth: {} | Union | {}[])
```

#### Target

`Namespace`
`union Namespace | Interface | Operation`

#### Parameters

Expand Down
1 change: 1 addition & 0 deletions docs/libraries/http/reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ npm install --save-peer @typespec/http
- [`ImplicitFlow`](./data-types.md#TypeSpec.Http.ImplicitFlow)
- [`LocationHeader`](./data-types.md#TypeSpec.Http.LocationHeader)
- [`MovedResponse`](./data-types.md#TypeSpec.Http.MovedResponse)
- [`NoAuth`](./data-types.md#TypeSpec.Http.NoAuth)
- [`NoContentResponse`](./data-types.md#TypeSpec.Http.NoContentResponse)
- [`NotFoundResponse`](./data-types.md#TypeSpec.Http.NotFoundResponse)
- [`NotModifiedResponse`](./data-types.md#TypeSpec.Http.NotModifiedResponse)
Expand Down
4 changes: 2 additions & 2 deletions packages/http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,15 +447,15 @@ op create(): {@statusCode: 201 | 202}

#### `@useAuth`

Specify this service authentication. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.
Specify authentication for a whole service or specific methods. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.

```typespec
@TypeSpec.Http.useAuth(auth: {} | Union | {}[])
```

##### Target

`Namespace`
`union Namespace | Interface | Operation`

##### Parameters

Expand Down
12 changes: 12 additions & 0 deletions packages/http/lib/auth.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ enum AuthType {

@doc("OpenID connect")
openIdConnect,

@doc("Empty auth")
noAuth,
}

/**
Expand Down Expand Up @@ -212,3 +215,12 @@ model OpenIdConnectAuth<ConnectUrl extends valueof string> {
/** Connect url. It can be specified relative to the server URL */
openIdConnectUrl: ConnectUrl;
}

/**
* This authentication option signifies that API is not secured at all.
* It might be useful when overriding authentication on interface of operation level.
*/
@doc("")
model NoAuth {
type: AuthType.noAuth;
}
4 changes: 2 additions & 2 deletions packages/http/lib/http-decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ extern dec server(
);

/**
* Specify this service authentication. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.
* Specify authentication for a whole service or specific methods. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details.
*
* @param auth Authentication configuration. Can be a single security scheme, a union(either option is valid authentication) or a tuple (must use all authentication together)
* @example
Expand All @@ -213,7 +213,7 @@ extern dec server(
* namespace PetStore;
* ```
*/
extern dec useAuth(target: Namespace, auth: {} | Union | {}[]);
extern dec useAuth(target: Namespace | Interface | Operation, auth: {} | Union | {}[]);

/**
* Specify if inapplicable metadata should be included in the payload for the given entity.
Expand Down
15 changes: 15 additions & 0 deletions packages/http/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Operation, Program } from "@typespec/compiler";
import { HttpStateKeys } from "./lib.js";
import { Authentication } from "./types.js";

export function getAuthenticationForOperation(
program: Program,
operation: Operation
): Authentication | undefined {
const operationAuth = program.stateMap(HttpStateKeys.authentication).get(operation);
if (operationAuth === undefined && operation.interface !== undefined) {
const interfaceAuth = program.stateMap(HttpStateKeys.authentication).get(operation.interface);
return interfaceAuth;
}
return operationAuth;
}
27 changes: 14 additions & 13 deletions packages/http/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
DecoratorContext,
Diagnostic,
DiagnosticTarget,
Interface,
Model,
ModelProperty,
Namespace,
Expand All @@ -25,6 +26,7 @@ import { HttpStateKeys, createDiagnostic, reportDiagnostic } from "./lib.js";
import { setRoute, setSharedRoute } from "./route.js";
import { getStatusCodesFromType } from "./status-codes.js";
import {
Authentication,
AuthenticationOption,
HeaderFieldOptions,
HttpAuth,
Expand All @@ -33,7 +35,6 @@ import {
HttpVerb,
PathParameterOptions,
QueryParameterOptions,
ServiceAuthentication,
} from "./types.js";
import { extractParamsFromPath } from "./utils.js";

Expand Down Expand Up @@ -432,28 +433,28 @@ setTypeSpecNamespace("Private", $plainData);

export function $useAuth(
context: DecoratorContext,
serviceNamespace: Namespace,
entity: Namespace | Interface | Operation,
authConfig: Model | Union | Tuple
) {
const [auth, diagnostics] = extractServiceAuthentication(context.program, authConfig);
const [auth, diagnostics] = extractAuthentication(context.program, authConfig);
if (diagnostics.length > 0) context.program.reportDiagnostics(diagnostics);
if (auth !== undefined) {
setAuthentication(context.program, serviceNamespace, auth);
setAuthentication(context.program, entity, auth);
}
}

export function setAuthentication(
program: Program,
serviceNamespace: Namespace,
auth: ServiceAuthentication
entity: Namespace | Interface | Operation,
auth: Authentication
) {
program.stateMap(HttpStateKeys.authentication).set(serviceNamespace, auth);
program.stateMap(HttpStateKeys.authentication).set(entity, auth);
}

function extractServiceAuthentication(
function extractAuthentication(
program: Program,
type: Model | Union | Tuple
): [ServiceAuthentication | undefined, readonly Diagnostic[]] {
): [Authentication | undefined, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();

switch (type.kind) {
Expand All @@ -473,7 +474,7 @@ function extractHttpAuthenticationOptions(
program: Program,
tuple: Union,
diagnosticTarget: DiagnosticTarget
): [ServiceAuthentication, readonly Diagnostic[]] {
): [Authentication, readonly Diagnostic[]] {
const options: AuthenticationOption[] = [];
const diagnostics = createDiagnosticCollector();
for (const variant of tuple.variants.values()) {
Expand Down Expand Up @@ -578,9 +579,9 @@ function extractOAuth2Auth(data: any): HttpAuth {

export function getAuthentication(
program: Program,
namespace: Namespace
): ServiceAuthentication | undefined {
return program.stateMap(HttpStateKeys.authentication).get(namespace);
entity: Namespace | Interface | Operation
): Authentication | undefined {
return program.stateMap(HttpStateKeys.authentication).get(entity);
}

/**
Expand Down
8 changes: 7 additions & 1 deletion packages/http/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
Program,
SyntaxKind,
} from "@typespec/compiler";
import { getAuthenticationForOperation } from "./auth.js";
import { getAuthentication } from "./decorators.js";
import { createDiagnostic, reportDiagnostic } from "./lib.js";
import { getResponsesForOperation } from "./responses.js";
import { isSharedRoute, resolvePathAndParameters } from "./route.js";
Expand Down Expand Up @@ -95,13 +97,15 @@ export function getHttpService(
},
})
);
const authentication = getAuthentication(program, serviceNamespace);

validateProgram(program, diagnostics);
validateRouteUnique(program, diagnostics, httpOperations);

const service: HttpService = {
namespace: serviceNamespace,
operations: httpOperations,
authentication: authentication,
};
return diagnostics.wrap(service);
}
Expand Down Expand Up @@ -213,15 +217,17 @@ function getHttpOperationInternal(
resolvePathAndParameters(program, operation, overloading, options ?? {})
);
const responses = diagnostics.pipe(getResponsesForOperation(program, operation));
const authentication = getAuthenticationForOperation(program, operation);

const httpOperation: HttpOperation = {
path: route.path,
pathSegments: route.pathSegments,
verb: route.parameters.verb,
container: operation.interface ?? operation.namespace ?? program.getGlobalNamespaceType(),
parameters: route.parameters,
operation,
responses,
operation,
authentication,
};
Object.assign(httpOperationRef, httpOperation);

Expand Down
19 changes: 17 additions & 2 deletions packages/http/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type OperationDetails = HttpOperation;

export type HttpVerb = "get" | "put" | "post" | "patch" | "delete" | "head";

export interface ServiceAuthentication {
export interface Authentication {
/**
* Either one of those options can be used independently to authenticate.
*/
Expand All @@ -35,7 +35,8 @@ export type HttpAuth =
| BearerAuth
| ApiKeyAuth<ApiKeyLocation, string>
| Oauth2Auth<OAuth2Flow[]>
| OpenIDConnectAuth;
| OpenIDConnectAuth
| NoAuth;

export interface HttpAuthBase {
/**
Expand Down Expand Up @@ -184,6 +185,14 @@ export interface OpenIDConnectAuth extends HttpAuthBase {
openIdConnectUrl: string;
}

/**
* This authentication option signifies that API is not secured at all.
* It might be useful when overriding authentication on interface of operation level.
*/
export interface NoAuth extends HttpAuthBase {
type: "noAuth";
}

export type OperationContainer = Namespace | Interface;

export type OperationVerbSelector = (
Expand Down Expand Up @@ -287,6 +296,7 @@ export interface HttpOperationParameters {
export interface HttpService {
namespace: Namespace;
operations: HttpOperation[];
authentication?: Authentication;
}

export interface HttpOperation {
Expand Down Expand Up @@ -325,6 +335,11 @@ export interface HttpOperation {
*/
operation: Operation;

/**
* Operation authentication. Overrides HttpService authentication
*/
authentication?: Authentication;

/**
* Overload this operation
*/
Expand Down
78 changes: 64 additions & 14 deletions packages/http/test/http-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,20 +623,6 @@ describe("http: decorators", () => {
});

describe("@useAuth", () => {
it("emit diagnostics when @useAuth is not used on namespace", async () => {
const diagnostics = await runner.diagnose(`
@useAuth(BasicAuth) op test(): string;
`);

expectDiagnostics(diagnostics, [
{
code: "decorator-wrong-target",
message:
"Cannot apply @useAuth decorator to test since it is not assignable to Namespace",
},
]);
});

it("emit diagnostics when config is not a model, tuple or union", async () => {
const diagnostics = await runner.diagnose(`
@useAuth(anOp)
Expand Down Expand Up @@ -792,6 +778,26 @@ describe("http: decorators", () => {
});
});

it("can specify NoAuth", async () => {
const { Foo } = (await runner.compile(`
@useAuth(NoAuth)
@test namespace Foo {}
`)) as { Foo: Namespace };

deepStrictEqual(getAuthentication(runner.program, Foo), {
options: [
{
schemes: [
{
id: "NoAuth",
type: "noAuth",
},
],
},
],
});
});

it("can specify multiple auth options", async () => {
const { Foo } = (await runner.compile(`
@useAuth(BasicAuth | BearerAuth)
Expand Down Expand Up @@ -853,6 +859,50 @@ describe("http: decorators", () => {
],
});
});

it("can override auth schemes on interface", async () => {
const { Foo } = (await runner.compile(`
alias ServiceKeyAuth = ApiKeyAuth<ApiKeyLocation.header, "X-API-KEY">;
@useAuth(ServiceKeyAuth)
@test namespace Foo {
@useAuth(BasicAuth | BearerAuth)
interface Bar { }
}
`)) as { Foo: Namespace };

deepStrictEqual(getAuthentication(runner.program, Foo.interfaces.get("Bar")!), {
options: [
{
schemes: [{ id: "BasicAuth", type: "http", scheme: "basic" }],
},
{
schemes: [{ id: "BearerAuth", type: "http", scheme: "bearer" }],
},
],
});
});

it("can override auth schemes on operation", async () => {
const { Foo } = (await runner.compile(`
alias ServiceKeyAuth = ApiKeyAuth<ApiKeyLocation.header, "X-API-KEY">;
@useAuth(ServiceKeyAuth)
@test namespace Foo {
@useAuth([BasicAuth, BearerAuth])
op bar(): void;
}
`)) as { Foo: Namespace };

deepStrictEqual(getAuthentication(runner.program, Foo.operations.get("bar")!), {
options: [
{
schemes: [
{ id: "BasicAuth", type: "http", scheme: "basic" },
{ id: "BearerAuth", type: "http", scheme: "bearer" },
],
},
],
});
});
});

describe("@visibility", () => {
Expand Down
Loading

0 comments on commit 4cd41e2

Please sign in to comment.