public async Task <TAuthorizationToken> GetAsync <TAuthorizationToken>(DeploymentScope deploymentScope) where TAuthorizationToken : AuthorizationToken, new() { if (deploymentScope is null) { throw new ArgumentNullException(nameof(deploymentScope)); } var client = await tableClient.ConfigureAwait(false); var rowKey = AuthorizationEntity.GetEntityId(deploymentScope); var partitionKey = string.Join(",", typeof(TAuthorizationToken).AssemblyQualifiedName.Split(',').Take(2)); try { var response = await client .GetEntityAsync <TAuthorizationToken>(partitionKey, rowKey) .ConfigureAwait(false); return(response.Value); } catch (RequestFailedException ex) when(ex.Status == 404) { // doesn't exist return(null); } }
public static string GetEntityId(DeploymentScope deploymentScope) { var entityId = Guid.Empty; if (deploymentScope is not null) { var result = Merge(Guid.Parse(deploymentScope.Organization), Guid.Parse(deploymentScope.Id)); entityId = new Guid(result.ToArray()); } return(entityId.ToString());
async Task <AzureServicePrincipal> IAdapterAuthorize.ResolvePrincipalAsync(DeploymentScope deploymentScope, HttpRequest request) { const string SignatureHeader = "X-Hub-Signature-256"; if (deploymentScope is null) { throw new ArgumentNullException(nameof(deploymentScope)); } if (request is null) { throw new ArgumentNullException(nameof(request)); } if (request.Headers.TryGetValue(SignatureHeader, out var signatureValue) && !string.IsNullOrEmpty(signatureValue)) { var token = await TokenClient .GetAsync <GitHubToken>(deploymentScope) .ConfigureAwait(false); if (!string.IsNullOrEmpty(token?.WebhookSecret)) { var signature = signatureValue .ToString() .Substring(signatureValue.ToString().IndexOf('=') + 1); // CAUTION - we need to read the body but leave // the request's body stream open so it stays // available for subsequent requests var body = await request .ReadStringAsync(leaveOpen : true) .ConfigureAwait(false); var secret = Encoding.ASCII .GetBytes(token.WebhookSecret); using var hmac = new HMACSHA256(secret); var hash = hmac .ComputeHash(Encoding.ASCII.GetBytes(body)) .ToHexString(); if (hash.Equals(signature)) { // signature successfully validated - lets use the adapters identity to proceed return(await GetServiceIdentityAsync(deploymentScope).ConfigureAwait(false)); } } } return(null); }
public override async Task <bool> IsAuthorizedAsync(DeploymentScope deploymentScope) { if (deploymentScope is null) { throw new ArgumentNullException(nameof(deploymentScope)); } var token = await TokenClient .GetAsync <GitHubToken>(deploymentScope) .ConfigureAwait(false); return(!(token is null)); }
private async Task <GitHubClient> CreateClientAsync(DeploymentScope deploymentScope, string acceptHeader = null) { if (deploymentScope is null) { throw new ArgumentNullException(nameof(deploymentScope)); } var token = await TokenClient .GetAsync <GitHubToken>(deploymentScope) .ConfigureAwait(false); return(await CreateClientAsync(token, acceptHeader) .ConfigureAwait(false)); }
public AzureDevOpsToken(DeploymentScope deployementScope) : base(GetEntityId(deployementScope)) { }
public GitHubToken(DeploymentScope deployementScope) : base(GetEntityId(deployementScope)) { }
public Task <IAuthorizationEndpoints> GetAuthorizationEndpointsAsync(DeploymentScope deploymentScope) => deploymentScope is null ? throw new ArgumentNullException(nameof(deploymentScope)) : Task.FromResult <IAuthorizationEndpoints>(new AuthorizationEndpoints()
protected override Task <Component> CreateComponentAsync(Component component, Organization componentOrganization, DeploymentScope componentDeploymentScope, Project componentProject, User contextUser, IAsyncCollector <ICommand> commandQueue) => ExecuteAsync(component, contextUser, commandQueue, async(client, ownerLogin, project, memberTeam, adminTeam) =>
async Task <IActionResult> IAdapterAuthorize.HandleCallbackAsync(DeploymentScope deploymentScope, HttpRequest request, IAuthorizationEndpoints authorizationEndpoints) { if (deploymentScope is null) { throw new ArgumentNullException(nameof(deploymentScope)); } if (request is null) { throw new ArgumentNullException(nameof(request)); } if (authorizationEndpoints is null) { throw new ArgumentNullException(nameof(authorizationEndpoints)); } var json = await request .ReadJsonAsync() .ConfigureAwait(false); var appId = json .SelectToken("installation.app_id")? .ToString(); var token = await TokenClient .GetAsync <GitHubToken>(deploymentScope) .ConfigureAwait(false); if (token?.ApplicationId?.Equals(appId, StringComparison.OrdinalIgnoreCase) ?? false) { var action = json .SelectToken("action")? .ToString(); switch (action) { case "created": token.InstallationId = json .SelectToken("installation.id")? .ToString(); break; case "deleted": token = new GitHubToken(deploymentScope); break; case "suspend": token.Suspended = true; break; case "unsuspend": token.Suspended = false; break; default: token = null; break; } if (token is not null) { _ = await TokenClient .SetAsync(token) .ConfigureAwait(false); return(new OkResult()); } } return(new NotFoundResult()); }
public GitHubSession(DeploymentScope deploymentScope = null) : base(GetEntityId(deploymentScope)) { }
protected override Task <Component> CreateComponentAsync(Component component, Organization organization, DeploymentScope deploymentScope, Project project, User contextUser, IAsyncCollector <ICommand> commandQueue) => WithKubernetesContext(component, deploymentScope, async(client, data, roleDefinition, serviceAccount) => { var componentNamespace = new V1Namespace() { Metadata = new V1ObjectMeta() { Name = $"{data.Namespace}-{component.Id}" } }; try { componentNamespace = await client .CreateNamespaceAsync(componentNamespace) .ConfigureAwait(false); } catch (HttpOperationException exc) when(exc.Response.StatusCode == System.Net.HttpStatusCode.Conflict) { componentNamespace = await client .ReadNamespaceAsync(componentNamespace.Metadata.Name) .ConfigureAwait(false); } var roleBinding = new V1RoleBinding() { Metadata = new V1ObjectMeta() { Name = "runner" }, RoleRef = new V1RoleRef() { ApiGroup = roleDefinition.ApiGroup(), Kind = roleDefinition.Kind, Name = roleDefinition.Name() }, Subjects = new List <V1Subject>() { new V1Subject() { ApiGroup = serviceAccount.ApiGroup(), Kind = serviceAccount.Kind, Name = serviceAccount.Name(), NamespaceProperty = serviceAccount.Namespace() } } }; try { await client .CreateNamespacedRoleBindingAsync(roleBinding, componentNamespace.Metadata.Name) .ConfigureAwait(false); } catch (HttpOperationException exc) when(exc.Response.StatusCode == System.Net.HttpStatusCode.Conflict) { await client .ReplaceNamespacedRoleBindingAsync(roleBinding, roleBinding.Metadata.Name, componentNamespace.Metadata.Name) .ConfigureAwait(false); } return(component); });
private KubernetesData GetKubernetesData(DeploymentScope deploymentScope) => TeamCloudSerialize.DeserializeObject <KubernetesData>(deploymentScope.InputData);
protected override async Task <Component> UpdateComponentAsync(Component component, Organization componentOrganization, DeploymentScope componentDeploymentScope, Project componentProject, User contextUser, IAsyncCollector <ICommand> commandQueue) { if (component is null) { throw new ArgumentNullException(nameof(component)); } if (AzureResourceIdentifier.TryParse(component.ResourceId, out var componentResourceId)) { var tasks = new Task[] { UpdateComponentRoleAssignmentsAsync(), UpdateComponentTagsAsync() }; await Task.WhenAll(tasks).ConfigureAwait(false); } return(component); async Task UpdateComponentRoleAssignmentsAsync() { var roleAssignmentMap = await GetRoleAssignmentsAsync(component) .ConfigureAwait(false); if (AzureResourceIdentifier.TryParse(component.IdentityId, out var identityResourceId)) { var session = await azureResourceService.AzureSessionService .CreateSessionAsync(identityResourceId.SubscriptionId) .ConfigureAwait(false); var identity = await session.Identities .GetByIdAsync(identityResourceId.ToString()) .ConfigureAwait(false); roleAssignmentMap .Add(identity.PrincipalId, Enumerable.Repeat(AzureRoleDefinition.Contributor, 1)); } if (string.IsNullOrEmpty(componentResourceId.ResourceGroup)) { var subscription = await azureResourceService .GetSubscriptionAsync(componentResourceId.SubscriptionId, throwIfNotExists : true) .ConfigureAwait(false); if (subscription is not null) { await subscription.SetRoleAssignmentsAsync(roleAssignmentMap).ConfigureAwait(false); } } else { var resourceGroup = await azureResourceService .GetResourceGroupAsync(componentResourceId.SubscriptionId, componentResourceId.ResourceGroup, throwIfNotExists : true) .ConfigureAwait(false); if (resourceGroup is not null) { await resourceGroup.SetRoleAssignmentsAsync(roleAssignmentMap).ConfigureAwait(false); } } } async Task UpdateComponentTagsAsync() { var tenantId = await azureResourceService.AzureSessionService .GetTenantIdAsync() .ConfigureAwait(false); var organization = await organizationRepository .GetAsync(tenantId.ToString(), component.Organization, true) .ConfigureAwait(false); var project = await projectRepository .GetAsync(component.Organization, component.ProjectId, true) .ConfigureAwait(false); var tags = organization.Tags .Union(project.Tags) .GroupBy(kvp => kvp.Key) .ToDictionary(g => g.Key, g => g.First().Value); if (string.IsNullOrEmpty(componentResourceId.ResourceGroup)) { var subscription = await azureResourceService .GetSubscriptionAsync(componentResourceId.SubscriptionId) .ConfigureAwait(false); if (subscription is not null) { await subscription.SetTagsAsync(tags, true).ConfigureAwait(false); } } else { var resourceGroup = await azureResourceService .GetResourceGroupAsync(componentResourceId.SubscriptionId, componentResourceId.ResourceGroup) .ConfigureAwait(false); if (resourceGroup is not null) { await resourceGroup.SetTagsAsync(tags, true).ConfigureAwait(false); } } } }
protected override Task <NetworkCredential> GetServiceCredentialAsync(Component component, Organization organization, DeploymentScope deploymentScope, Project project) => WithKubernetesContext(component, deploymentScope, async(client, data, roleDefinition, serviceAccount) => { var configuration = await client .CreateClusterConfigAsync(serviceAccount) .ConfigureAwait(false); return(new NetworkCredential() { Domain = client.BaseUri.ToString(), Password = new SerializerBuilder().Build().Serialize(configuration), UserName = serviceAccount.Name() }); });
protected override Task <Component> UpdateComponentAsync(Component component, Organization organization, DeploymentScope deploymentScope, Project project, User contextUser, IAsyncCollector <ICommand> commandQueue) => WithKubernetesContext(component, deploymentScope, (client, data, roleDefinition, serviceAccount) => { //TODO: implement some update logic - e.g. permission management for project users return(Task.FromResult(component)); });
protected override Task <Component> DeleteComponentAsync(Component component, Organization organization, DeploymentScope deploymentScope, Project project, User contextUser, IAsyncCollector <ICommand> commandQueue) => WithKubernetesContext(component, deploymentScope, async(client, data, roleDefinition, serviceAccount) => { try { await client .DeleteNamespaceAsync($"{data.Namespace}-{component.Id}") .ConfigureAwait(false); } catch (HttpOperationException exc) when(exc.Response.StatusCode == System.Net.HttpStatusCode.NotFound) { // swallow - the namespace was already deleted } return(component); });
public AzureDevOpsSession(DeploymentScope deploymentScope) : base(GetEntityId(deploymentScope)) { }
async Task <IActionResult> IAdapterAuthorize.HandleAuthorizeAsync(DeploymentScope deploymentScope, HttpRequest request, IAuthorizationEndpoints authorizationEndpoints) { if (deploymentScope is null) { throw new ArgumentNullException(nameof(deploymentScope)); } if (request is null) { throw new ArgumentNullException(nameof(request)); } if (authorizationEndpoints is null) { throw new ArgumentNullException(nameof(authorizationEndpoints)); } var queryParams = Url.ParseQueryParams(request.QueryString.ToString()); var queryState = queryParams.GetValueOrDefault("state"); var queryCode = queryParams.GetValueOrDefault("code"); var queryError = queryParams.GetValueOrDefault("error"); var session = await SessionClient .GetAsync <GitHubSession>(deploymentScope) .ConfigureAwait(false); session ??= await SessionClient .SetAsync(new GitHubSession(deploymentScope)) .ConfigureAwait(false); var data = string.IsNullOrWhiteSpace(deploymentScope.InputData) ? default : TeamCloudSerialize.DeserializeObject <GitHubData>(deploymentScope.InputData); var token = await TokenClient .GetAsync <GitHubToken>(deploymentScope) .ConfigureAwait(false); if (string.IsNullOrEmpty(queryError) && Guid.TryParse(queryState, out var stateId)) { if (!stateId.ToString().Equals(session.SessionId, StringComparison.OrdinalIgnoreCase)) { return(new RedirectResult(authorizationEndpoints.AuthorizationUrl.SetQueryParam("error", "Session timed out.").ToString())); } else if (string.IsNullOrWhiteSpace(queryCode)) { return(new RedirectResult(authorizationEndpoints.AuthorizationUrl.SetQueryParam("error", "Missing GitHub handshake information.").ToString())); } token ??= new GitHubToken(deploymentScope); // Using Flurl as Octokit doesn't support this API yet // https://github.com/octokit/octokit.net/issues/2138 var url = $"https://api.github.com/app-manifests/{queryCode}/conversions"; var response = await url .WithHeader("User-Agent", GitHubConstants.ProductHeader.ToString()) .PostStringAsync(string.Empty) .ConfigureAwait(false); if (!response.IsSuccessStatusCode()) { return(new RedirectResult(authorizationEndpoints.AuthorizationUrl.SetQueryParam("error", $"Failed to get application token ({response.StatusCode} - {response.ResponseMessage.ReasonPhrase}).").ToString())); } var json = await response .GetStringAsync() .ConfigureAwait(false); TeamCloudSerialize.PopulateObject(json, token); if (string.IsNullOrEmpty(token.OwnerLogin)) { token.OwnerLogin = JToken.Parse(json) .SelectToken("owner.login") .ToString(); } token = await TokenClient .SetAsync(token, true) .ConfigureAwait(false); return(new ContentResult { StatusCode = (int)HttpStatusCode.OK, ContentType = "text/html", Content = Assembly.GetExecutingAssembly().GetManifestResourceTemplate($"{GetType().FullName}_Install.html", GetFormContext()) }); } else { return(new ContentResult { StatusCode = (int)HttpStatusCode.OK, ContentType = "text/html", Content = Assembly.GetExecutingAssembly().GetManifestResourceTemplate($"{GetType().FullName}_Register.html", GetFormContext()) }); } object GetFormContext() => new { deploymentScope = deploymentScope, authorizationEndpoints = authorizationEndpoints, token = token, data = data, session = session, error = queryError ?? string.Empty, succeeded = queryParams.Contains("succeeded") }; }
protected override async Task <Component> CreateComponentAsync(Component component, Organization componentOrganization, DeploymentScope componentDeploymentScope, Project componentProject, User contextUser, IAsyncCollector <ICommand> commandQueue) { if (component is null) { throw new ArgumentNullException(nameof(component)); } if (!AzureResourceIdentifier.TryParse(component.ResourceId, out var resourceId)) { component.ResourceId = await CreateResourceIdAsync(component) .ConfigureAwait(false); resourceId = AzureResourceIdentifier.Parse(component.ResourceId); var sessionIdenity = await azureResourceService.AzureSessionService .GetIdentityAsync() .ConfigureAwait(false); component.ResourceUrl = resourceId.GetPortalUrl(sessionIdenity.TenantId); component = await componentRepository .SetAsync(component) .ConfigureAwait(false); } return(await UpdateComponentAsync(component, contextUser, commandQueue).ConfigureAwait(false)); }
private async Task <T> WithKubernetesContext <T>(Component component, DeploymentScope deploymentScope, Func <IKubernetes, KubernetesData, V1ClusterRole, V1ServiceAccount, Task <T> > callback) { if (component is null) { throw new ArgumentNullException(nameof(component)); } if (deploymentScope is null) { throw new ArgumentNullException(nameof(deploymentScope)); } if (callback is null) { throw new ArgumentNullException(nameof(callback)); } var identity = await azureResourceService .GetResourceAsync <AzureIdentityResource>(component.IdentityId, throwIfNotExists : true) .ConfigureAwait(false); var data = GetKubernetesData(deploymentScope); var client = GetKubernetesClient(data); var roleDefinition = new V1ClusterRole() { Metadata = new V1ObjectMeta() { Name = "teamcloud-runner" }, Rules = new List <V1PolicyRule>() { new V1PolicyRule() { ApiGroups = new List <string>() { "", "extensions", "apps" }, Resources = new List <string>() { "*" }, Verbs = new List <string>() { "*" } }, new V1PolicyRule() { ApiGroups = new List <string>() { "batch" }, Resources = new List <string>() { "jobs", "cronjobs" }, Verbs = new List <string>() { "*" } } } }; try { roleDefinition = await client .CreateClusterRoleAsync(roleDefinition) .ConfigureAwait(false); } catch (HttpOperationException exc) when(exc.Response.StatusCode == System.Net.HttpStatusCode.Conflict) { roleDefinition = await client .ReadClusterRoleAsync(roleDefinition.Metadata.Name) .ConfigureAwait(false); } var serviceAccount = new V1ServiceAccount() { Metadata = new V1ObjectMeta() { Name = $"{data.Namespace}-{identity.PrincipalId}" } }; try { serviceAccount = await client .CreateNamespacedServiceAccountAsync(serviceAccount, data.Namespace) .ConfigureAwait(false); } catch (HttpOperationException exc) when(exc.Response.StatusCode == System.Net.HttpStatusCode.Conflict) { serviceAccount = await client .ReadNamespacedServiceAccountAsync(serviceAccount.Metadata.Name, data.Namespace) .ConfigureAwait(false); } return(await callback(client, data, roleDefinition, serviceAccount).ConfigureAwait(false)); }
protected override async Task <Component> DeleteComponentAsync(Component component, Organization componentOrganization, DeploymentScope componentDeploymentScope, Project componentProject, User contextUser, IAsyncCollector <ICommand> commandQueue) { if (component is null) { throw new ArgumentNullException(nameof(component)); } if (AzureResourceIdentifier.TryParse(component.ResourceId, out var componentResourceId)) { var resourceGroup = await azureResourceService .GetResourceGroupAsync(componentResourceId.SubscriptionId, componentResourceId.ResourceGroup) .ConfigureAwait(false); if (resourceGroup is not null) { await resourceGroup .DeleteAsync(true) .ConfigureAwait(false); } // remove resource related informations component.ResourceId = null; component.ResourceUrl = null; // ensure resource state is deleted component.ResourceState = Model.Common.ResourceState.Deprovisioned; // update entity to ensure we have it's state updated in case the delete fails component = await componentRepository .SetAsync(component) .ConfigureAwait(false); } return(component); }
public virtual async Task <AzureServicePrincipal> GetServiceIdentityAsync(DeploymentScope deploymentScope, bool withPassword = false) { if (this is IAdapterIdentity) { var servicePrincipalKey = Guid.Parse(deploymentScope.Organization) .Combine(Guid.Parse(deploymentScope.Id)); var servicePrincipalName = $"{this.GetType().Name}/{servicePrincipalKey}"; var servicePrincipal = await graphService .GetServicePrincipalAsync(servicePrincipalName) .ConfigureAwait(false); if (servicePrincipal is null) { // there is no service principal for the current deployment scope // create a new one that we can use to create/update the corresponding // service endpoint in the current team project servicePrincipal = await graphService .CreateServicePrincipalAsync(servicePrincipalName) .ConfigureAwait(false); } else if (servicePrincipal.ExpiresOn.GetValueOrDefault(DateTime.MinValue) < DateTime.UtcNow) { // a service principal exists, but its secret is expired. lets refresh // the service principal (create a new secret) so we can move on // creating/updating the corresponding service endpoint. servicePrincipal = await graphService .RefreshServicePrincipalAsync(servicePrincipalName) .ConfigureAwait(false); } if (!string.IsNullOrEmpty(servicePrincipal.Password)) { var tenantId = await azureSessionService .GetTenantIdAsync() .ConfigureAwait(false); var organization = await organizationRepository .GetAsync(tenantId.ToString(), deploymentScope.Organization) .ConfigureAwait(false); var secretsStore = await secretsStoreProvider .GetSecretsStoreAsync(organization) .ConfigureAwait(false); servicePrincipal = await secretsStore .SetSecretAsync(servicePrincipal.Id.ToString(), servicePrincipal) .ConfigureAwait(false); } else if (withPassword) { var tenantId = await azureSessionService .GetTenantIdAsync() .ConfigureAwait(false); var organization = await organizationRepository .GetAsync(tenantId.ToString(), deploymentScope.Organization) .ConfigureAwait(false); var secretsStore = await secretsStoreProvider .GetSecretsStoreAsync(organization) .ConfigureAwait(false); servicePrincipal = (await secretsStore .GetSecretAsync <AzureServicePrincipal>(servicePrincipal.Id.ToString()) .ConfigureAwait(false)) ?? servicePrincipal; } return(servicePrincipal); } return(null); }