Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/private apis #1991

Merged
merged 42 commits into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8a7991c
- adds basic infrastructure to authenticate to github
baywet Nov 22, 2022
d3206a3
- implements login command and search with private repos
baywet Nov 22, 2022
47f7d9c
- adds a logout command
baywet Nov 22, 2022
bf8faac
- adds missing parameters to builder
baywet Nov 22, 2022
ab30973
- code linting
baywet Nov 23, 2022
02973a6
- adds base infrastructure for browser authentication
baywet Nov 23, 2022
b5e8582
- adds session storage integration for state
baywet Nov 23, 2022
b1c7ae1
- adds query parameters read
baywet Nov 23, 2022
a4c3c38
- moves authentication providers to their target environment
baywet Nov 24, 2022
60783cd
- adds sign in button
baywet Nov 24, 2022
252543d
- moves the authentication configuration to its own extension method
baywet Nov 24, 2022
5fc00fe
- adds a github icon for the sign in button
baywet Nov 24, 2022
9fc702e
- adds a pat provider for github sign in
baywet Nov 24, 2022
ec800d6
- replaces session storage by local storage to persist across sessions
baywet Nov 25, 2022
7e62611
- removes dependency to temp folder service in kiota web to simplify …
baywet Nov 25, 2022
1405ef1
- moves search terms outside of configuration to simplify Kiota searc…
baywet Nov 25, 2022
11e4297
- adds failsafe when unable to access files
baywet Nov 25, 2022
d65720b
- polishes UI for github sign in
baywet Nov 25, 2022
e51aef3
- adds missing alt text
baywet Nov 25, 2022
1c8082d
- code linting
baywet Nov 25, 2022
4176ff1
- replaces svg alt by title
baywet Nov 25, 2022
f321f41
- moves device login to a subcommand
baywet Nov 28, 2022
c228597
- refactors token caching
baywet Nov 28, 2022
0986ff7
- moves device code provide to right folder
baywet Nov 28, 2022
4b4c47a
- linting
baywet Nov 28, 2022
2ba3371
- adds pat sign in option for CLI
baywet Nov 28, 2022
04f71f0
- enables nullable for cli project
baywet Nov 28, 2022
bb0ca63
- adds pat sign in command
baywet Nov 28, 2022
4638fcf
- minor fix after refactoring
baywet Nov 28, 2022
b26212e
- code linting
baywet Nov 29, 2022
32aa6e0
- adds documentation for the login and logout commands
baywet Nov 29, 2022
8e2468e
- adds missing unit tests for github authentication
baywet Nov 29, 2022
207d44f
- adds a tool tip to the sign in button
baywet Nov 29, 2022
fac8e61
- locks the github api version
baywet Nov 30, 2022
0276638
- fixes data types after client refresh
baywet Nov 30, 2022
19ab2cc
- adds installations to github client
baywet Nov 30, 2022
a48a094
- switches to modern github app for device code
baywet Nov 30, 2022
add6960
- adds a changelog entry for sign in commands
baywet Nov 30, 2022
58a82e8
Apply suggestions from code review
baywet Dec 1, 2022
3c8d25e
- applies missing recommendations from PR
baywet Dec 1, 2022
7a994db
- fixes typo in file name
baywet Dec 1, 2022
3a929a3
- fixes stack overflow with token cache service
baywet Dec 1, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,20 @@
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Launch Login (github - device)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/kiota/bin/Debug/net7.0/kiota.dll",
"args": ["login",
"github",
"device"
],
"cwd": "${workspaceFolder}/src/kiota",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Added support for GitHub based API search.[#1866](https://github.com/microsoft/kiota/issues/1866)
- Added login/logout commands to access API descriptions in private GitHub repositories. [#1983](https://github.com/microsoft/kiota/issues/1983)
- Added support for scalar request bodies Python [#1571](https://github.com/microsoft/kiota/issues/1571)
- Sets property defaults in constructor and removes duplicate properties defined in base types from model serialization and deserialization methods in Python. [#1726](https://github.com/microsoft/kiota/issues/1726)
- Added support for scalar request bodies in PHP [#1937](https://github.com/microsoft/kiota/pull/1937)
Expand Down
41 changes: 41 additions & 0 deletions docs/using.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Kiota offers the following commands to help you build your API client:
- [generate](#client-generation): generate a client for any API from its description.
- [update](#client-update): update existing clients from previous generations.
- [info](#language-information): show languages and runtime dependencies information.
- [login](#login): logs in to private API descriptions repositories.
- [logout](#logout): logs out from private API description repositories.

## Description search

Expand Down Expand Up @@ -402,6 +404,45 @@ The generate command accepts optional parameters commonly available on the other
- [--log-level](#--log-level---ll)
- [--output](#--output--o)

## Login

Use kiota login to sign in to private repositories and search for/display/generate clients for private API descriptions. This command makes sub-commands available to sign in to specific authentication providers.

### Login to GitHub

```shell
kiota login github <device|pat>
[(--log-level | --ll) <level>]
[(--pat) <patValue>]
```

Use `kiota login github device` to sign in using a device code, you will be prompted to sign-in using a web browser.

Use `kiota login github pat --pat patValue` to sign in using a Personal Access Token you previously generated. You can use both classic PATs or granular PATs (beta). Classic PATs need the **repo** permission and to be granted access to the target organizations. Granular PATs need a **read-only** permission for the **contents** scope under the **repository** category and they need to be consented for all private repositories or selected private repositories.

> Note: for more information about adding API descriptions to the GitHub index, see [Adding an API to search](add-api.md)

### Optional parameters

The generate command accepts optional parameters commonly available on the other commands:

- [--log-level](#--log-level---ll)

## Logout

Use kiota logout to logout from a private repository containing API descriptions.

```shell
kiota logout github
[(--log-level | --ll) <level>]
```

### Optional parameters

The generate command accepts optional parameters commonly available on the other commands:

- [--log-level](#--log-level---ll)

## Common parameters

The following parameters are available across multiple commands.
Expand Down
9 changes: 8 additions & 1 deletion src/Kiota.Builder/Configuration/SearchConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@ namespace Kiota.Builder.Configuration;

public class SearchConfiguration : SearchConfigurationBase {
public Uri APIsGuruListUrl { get; set; } = new ("https://raw.githubusercontent.com/APIs-guru/openapi-directory/gh-pages/v2/list.json");
public Uri GitHubBlockListUrl { get; set; } = new ("https://raw.githubusercontent.com/microsoft/kiota/main/resources/index-block-list.yml");
public GitHubConfiguration GitHub { get; set; } = new();
}

public class GitHubConfiguration {
public string AppId { get; set; } = "Iv1.9ed2bcb878c90617";
public Uri ApiBaseUrl { get; set; } = new ("https://api.github.com");
public Uri BlockListUrl { get; set; } = new ("https://raw.githubusercontent.com/microsoft/kiota/main/resources/index-block-list.yml");
public Uri AppManagement { get; set; } = new("https://aka.ms/kiota/install/github");
}
2 changes: 0 additions & 2 deletions src/Kiota.Builder/Configuration/SearchConfigurationBase.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
namespace Kiota.Builder.Configuration;

public abstract class SearchConfigurationBase {
public string SearchTerm { get; set; }
public bool ClearCache { get; set; }
public string Version { get; set; }
}
11 changes: 11 additions & 0 deletions src/Kiota.Builder/Extensions/UriExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.IO;

namespace Kiota.Builder.Extensions;

public static class UriExtensions {
public static string GetFileName(this Uri uri) {
if(uri is null) return string.Empty;
return Path.GetFileName($"{uri.Scheme}://{uri.Host}{uri.AbsolutePath}");
}
}
13 changes: 9 additions & 4 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,19 @@ public class KiotaBuilder
{
private readonly ILogger<KiotaBuilder> logger;
private readonly GenerationConfiguration config;
private readonly HttpClient httpClient;
private OpenApiDocument originalDocument;
private OpenApiDocument openApiDocument;
internal void SetOpenApiDocument(OpenApiDocument document) => openApiDocument = document ?? throw new ArgumentNullException(nameof(document));

public KiotaBuilder(ILogger<KiotaBuilder> logger, GenerationConfiguration config)
public KiotaBuilder(ILogger<KiotaBuilder> logger, GenerationConfiguration config, HttpClient client)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(config);
ArgumentNullException.ThrowIfNull(client);
this.logger = logger;
this.config = config;
this.httpClient = client;
}
private void CleanOutputDirectory()
{
Expand Down Expand Up @@ -222,12 +227,12 @@ private async Task<Stream> LoadStream(string inputPath, CancellationToken cancel
Stream input;
if (inputPath.StartsWith("http"))
try {
using var httpClient = new HttpClient();
var cachingProvider = new DocumentCachingProvider(httpClient, logger) {
ClearCache = config.ClearCache,
};
var fileName = Path.GetFileName(inputPath) is string name && !string.IsNullOrEmpty(name) ? name : "description.yml";
input = await cachingProvider.GetDocumentAsync(new Uri(inputPath), "generation", fileName, cancellationToken: cancellationToken);
var targetUri = new Uri(inputPath);
var fileName = targetUri.GetFileName() is string name && !string.IsNullOrEmpty(name) ? name : "description.yml";
input = await cachingProvider.GetDocumentAsync(targetUri, "generation", fileName, cancellationToken: cancellationToken);
} catch (HttpRequestException ex) {
throw new InvalidOperationException($"Could not download the file at {inputPath}, reason: {ex.Message}", ex);
}
Expand Down
44 changes: 26 additions & 18 deletions src/Kiota.Builder/KiotaSearcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,47 @@
using Kiota.Builder.SearchProviders.GitHub;
using Kiota.Builder.SearchProviders.MSGraph;
using Microsoft.Extensions.Logging;
using Microsoft.Kiota.Abstractions.Authentication;

namespace Kiota.Builder;

public class KiotaSearcher {
private readonly ILogger<KiotaSearcher> logger;
private readonly SearchConfiguration config;
public KiotaSearcher(ILogger<KiotaSearcher> logger, SearchConfiguration config) {
private readonly ILogger<KiotaSearcher> _logger;
private readonly SearchConfiguration _config;
private readonly HttpClient _httpClient;
private readonly IAuthenticationProvider _gitHubAuthenticationProvider;
private readonly Func<CancellationToken, Task<bool>> _isGitHubSignedInCallBack;

public KiotaSearcher(ILogger<KiotaSearcher> logger, SearchConfiguration config, HttpClient httpClient, IAuthenticationProvider gitHubAuthenticationProvider, Func<CancellationToken, Task<bool>> isGitHubSignedInCallBack) {
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(config);
this.logger = logger;
this.config = config;
ArgumentNullException.ThrowIfNull(httpClient);
_logger = logger;
_config = config;
_httpClient = httpClient;
_gitHubAuthenticationProvider = gitHubAuthenticationProvider;
_isGitHubSignedInCallBack = isGitHubSignedInCallBack;
}
public async Task<IDictionary<string, SearchResult>> SearchAsync(CancellationToken cancellationToken) {
if (string.IsNullOrEmpty(config.SearchTerm)) {
logger.LogError("no search term provided");
public async Task<IDictionary<string, SearchResult>> SearchAsync(string searchTerm, string version, CancellationToken cancellationToken) {
if (string.IsNullOrEmpty(searchTerm)) {
_logger.LogError("no search term provided");
return new Dictionary<string, SearchResult>();
}
using var client = new HttpClient();
var apiGurusSearchProvider = new APIsGuruSearchProvider(config.APIsGuruListUrl, client, logger, config.ClearCache);
logger.LogDebug("searching for {searchTerm}", config.SearchTerm);
logger.LogDebug("searching APIs.guru with url {url}", config.APIsGuruListUrl);
var apiGurusSearchProvider = new APIsGuruSearchProvider(_config.APIsGuruListUrl, _httpClient, _logger, _config.ClearCache);
_logger.LogDebug("searching for {searchTerm}", searchTerm);
_logger.LogDebug("searching APIs.guru with url {url}", _config.APIsGuruListUrl);
var oasProvider = new OpenApiSpecSearchProvider();
var githubProvider = new GitHubSearchProvider(client, config.GitHubBlockListUrl, logger, config.ClearCache);
var githubProvider = new GitHubSearchProvider(_httpClient, _logger, _config.ClearCache, _config.GitHub, _gitHubAuthenticationProvider, _isGitHubSignedInCallBack);
var results = await Task.WhenAll(
SearchProviderAsync(apiGurusSearchProvider, cancellationToken),
SearchProviderAsync(oasProvider, cancellationToken),
SearchProviderAsync(githubProvider, cancellationToken));
SearchProviderAsync(searchTerm, version, apiGurusSearchProvider, cancellationToken),
SearchProviderAsync(searchTerm, version, oasProvider, cancellationToken),
SearchProviderAsync(searchTerm, version, githubProvider, cancellationToken));
return results.SelectMany(static x => x)
.ToDictionary(static x => x.Key, static x => x.Value, StringComparer.OrdinalIgnoreCase);
}
private async Task<IDictionary<string, SearchResult>> SearchProviderAsync(ISearchProvider provider, CancellationToken cancellationToken) {
private static async Task<IDictionary<string, SearchResult>> SearchProviderAsync(string searchTerm, string version, ISearchProvider provider, CancellationToken cancellationToken) {
var providerPrefix = $"{provider.ProviderKey}{ProviderSeparator}";
var results = await provider.SearchAsync(config.SearchTerm.Replace(providerPrefix, string.Empty), config.Version, cancellationToken);
var results = await provider.SearchAsync(searchTerm.Replace(providerPrefix, string.Empty), version, cancellationToken);

return results.Select(x => ($"{providerPrefix}{x.Key}", x.Value))
.ToDictionary(static x => x.Item1, static x => x.Value, StringComparer.OrdinalIgnoreCase);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Text.Json.Serialization;

namespace Kiota.Builder.SearchProviders.GitHub.Authentication;

public class AccessCodeResponse {
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("error_description")]
public string ErrorDescription { get; set; }
[JsonPropertyName("error_uri")]
public Uri ErrorUri { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
using Microsoft.Kiota.Abstractions.Authentication;

namespace Kiota.Builder.SearchProviders.GitHub.Authentication;
public class GitHubAnonymousAuthenticationProvider : IAuthenticationProvider
public class AnonymousAuthenticationProvider : IAuthenticationProvider
{
public Task AuthenticateRequestAsync(RequestInformation request, Dictionary<string, object> additionalAuthenticationContext = null, CancellationToken cancellationToken = default)
public virtual Task AuthenticateRequestAsync(RequestInformation request, Dictionary<string, object> additionalAuthenticationContext = null, CancellationToken cancellationToken = default)
{
request.Headers.Add("User-Agent", $"Kiota/{GetType().Assembly.GetName().Version}");
request.Headers.Add("X-GitHub-Api-Version", "2022-11-28");
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;

namespace Kiota.Builder.SearchProviders.GitHub.Authentication;

public class BaseAuthenticationProvider<T> : AnonymousAuthenticationProvider where T: class, IAccessTokenProvider
{
public BaseAuthenticationProvider(string clientId,
string scope,
IEnumerable<string> validHosts,
ILogger logger,
Func<string, string, IEnumerable<string>, T> accessTokenProviderFactory,
bool enableCache = true)
{
ArgumentNullException.ThrowIfNull(validHosts);
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(accessTokenProviderFactory);
if (string.IsNullOrEmpty(clientId))
throw new ArgumentNullException(nameof(clientId));
if (string.IsNullOrEmpty(scope))
throw new ArgumentNullException(nameof(scope));

AccessTokenProvider = accessTokenProviderFactory(clientId, scope, validHosts);
if(enableCache)
AccessTokenProvider = new TempFolderCachingAccessTokenProvider {
Concrete = AccessTokenProvider,
Logger = logger,
ApiBaseUrl = new Uri($"https://{validHosts.FirstOrDefault() ?? "api.github.com"}"),
AppId = clientId,
};
}
public IAccessTokenProvider AccessTokenProvider {get; private set;}
private const string AuthorizationHeaderKey = "Authorization";
private const string ClaimsKey = "claims";
public override Task AuthenticateRequestAsync(RequestInformation request, Dictionary<string, object> additionalAuthenticationContext = null, CancellationToken cancellationToken = default) {
ArgumentNullException.ThrowIfNull(request);
return AuthenticateRequestInternalAsync(request, additionalAuthenticationContext, cancellationToken);
}
private async Task AuthenticateRequestInternalAsync(RequestInformation request, Dictionary<string, object> additionalAuthenticationContext = null, CancellationToken cancellationToken = default) {
await base.AuthenticateRequestAsync(request, additionalAuthenticationContext, cancellationToken).ConfigureAwait(false);
if(additionalAuthenticationContext != null &&
additionalAuthenticationContext.ContainsKey(ClaimsKey) &&
request.Headers.ContainsKey(AuthorizationHeaderKey))
request.Headers.Remove(AuthorizationHeaderKey);

if(!request.Headers.ContainsKey(AuthorizationHeaderKey))
{
var token = await AccessTokenProvider.GetAuthorizationTokenAsync(request.URI, additionalAuthenticationContext, cancellationToken);
if(!string.IsNullOrEmpty(token))
request.Headers.Add(AuthorizationHeaderKey, $"Bearer {token}");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;

namespace Kiota.Builder.SearchProviders.GitHub.Authentication;

public interface ITokenStorageService {
Task<string> GetTokenAsync(CancellationToken cancellationToken);
Task SetTokenAsync(string value, CancellationToken cancellationToken);
Task<bool> IsTokenPresentAsync(CancellationToken cancellationToken);
Task<bool> DeleteTokenAsync(CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions.Authentication;

namespace Kiota.Builder.SearchProviders.GitHub.Authentication;

public class PatAccessTokenProvider : IAccessTokenProvider
{
public AllowedHostsValidator AllowedHostsValidator { get; set; } = new();
public required ITokenStorageService StorageService { get; init; }
public Task<string> GetAuthorizationTokenAsync(Uri uri, Dictionary<string, object> additionalAuthenticationContext = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(uri);
if("https".Equals(uri.Scheme, StringComparison.OrdinalIgnoreCase) && AllowedHostsValidator.IsUrlHostValid(uri))
return StorageService.GetTokenAsync(cancellationToken);
return Task.FromResult(string.Empty);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;

namespace Kiota.Builder.SearchProviders.GitHub.Authentication;
public class PatAuthenticationProvider : BaseAuthenticationProvider<PatAccessTokenProvider>
{
public PatAuthenticationProvider(string clientId, string scope, IEnumerable<string> validHosts, ILogger logger, ITokenStorageService StorageService) :
base(clientId, scope, validHosts, logger, (_, _, validHosts) => new PatAccessTokenProvider {
StorageService = StorageService,
AllowedHostsValidator = new (validHosts),
}, false)
{
ArgumentNullException.ThrowIfNull(StorageService);
}
}
Loading