sources/Google.Solutions.Apis/Crm/ResourceManagerClient.cs (253 lines of code) (raw):

// // Copyright 2020 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Apis.CloudResourceManager.v1; using Google.Apis.CloudResourceManager.v1.Data; using Google.Apis.Requests; using Google.Solutions.Apis.Auth; using Google.Solutions.Apis.Client; using Google.Solutions.Apis.Locator; using Google.Solutions.Common.Diagnostics; using Google.Solutions.Common.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Google.Solutions.Apis.Crm { /// <summary> /// Client for Resource Manager (CRM) API. /// </summary> public interface IResourceManagerClient : IClient { /// <summary> /// Get project details. /// </summary> /// <exception cref="ResourceAccessDeniedException">when access denied</exception> Task<Project> GetProjectAsync( ProjectLocator project, CancellationToken cancellationToken); /// <summary> /// Search for projects. /// </summary> Task<FilteredProjectList> ListProjectsAsync( ProjectFilter? filter, int? maxResults, CancellationToken cancellationToken); /// <summary> /// Test if all permissions have been granted. /// </summary> Task<bool> IsAccessGrantedAsync( ProjectLocator project, IReadOnlyCollection<string> permissions, CancellationToken cancellationToken); /// <summary> /// Lookup parent organization for a project. /// </summary> /// <returns> /// Locator or null if the parent doesn't belong to an organization /// or the user has insifficient permission to lookup the parent organization /// </returns> Task<OrganizationLocator?> FindOrganizationAsync( ProjectLocator project, CancellationToken cancellationToken); /// <summary> /// Get organization details. /// </summary> /// <exception cref="ResourceAccessDeniedException">when access denied</exception> Task<Organization> GetOrganizationAsync( OrganizationLocator organization, CancellationToken cancellationToken); } public sealed class ResourceManagerClient : ApiClientBase, IResourceManagerClient, IDisposable { private readonly CloudResourceManagerService service; public ResourceManagerClient( ServiceEndpoint<ResourceManagerClient> endpoint, IAuthorization authorization, UserAgent userAgent) : base(endpoint, authorization, userAgent) { this.service = new CloudResourceManagerService(this.Initializer); } public static ServiceEndpoint<ResourceManagerClient> CreateEndpoint( ServiceRoute? route = null) { return new ServiceEndpoint<ResourceManagerClient>( route ?? ServiceRoute.Public, "https://cloudresourcemanager.googleapis.com/"); } //--------------------------------------------------------------------- // IResourceManagerClient. //--------------------------------------------------------------------- public async Task<Project> GetProjectAsync( ProjectLocator project, CancellationToken cancellationToken) { using (ApiTraceSource.Log.TraceMethod().WithParameters(project)) { try { return await this.service.Projects .Get(project.Name) .ExecuteAsync(cancellationToken) .ConfigureAwait(false); } catch (GoogleApiException e) when (e.IsAccessDenied()) { throw new ResourceAccessDeniedException( $"You do not have sufficient permissions to access project {project.Name}. " + "You need the 'Compute Viewer' role (or an equivalent custom role) " + "to perform this action.", HelpTopics.ProjectAccessControl, e); } } } public async Task<FilteredProjectList> ListProjectsAsync( ProjectFilter? filter, int? maxResults, CancellationToken cancellationToken) { using (ApiTraceSource.Log.TraceMethod().WithParameters(filter)) { var request = new ProjectsResource.ListRequest(this.service) { Filter = filter?.ToString(), PageSize = maxResults }; IList<Project> projects; bool truncated; if (maxResults.HasValue) { // Read single page. var response = await request .ExecuteAsync(cancellationToken) .ConfigureAwait(false); truncated = response.NextPageToken != null; projects = response.Projects; } else { // Read all pages. truncated = false; projects = await new PageStreamer< Project, ProjectsResource.ListRequest, ListProjectsResponse, string>( (req, token) => req.PageToken = token, response => response.NextPageToken, response => response.Projects) .FetchAllAsync(request, cancellationToken) .ConfigureAwait(false); } // Filter projects in deleted/pending delete state. var activeProjects = projects .EnsureNotNull() .Where(p => p.LifecycleState == "ACTIVE"); ApiTraceSource.Log.TraceVerbose( "Found {0} projects", activeProjects.Count()); return new FilteredProjectList(activeProjects, truncated); } } public async Task<bool> IsAccessGrantedAsync( ProjectLocator project, IReadOnlyCollection<string> permissions, CancellationToken cancellationToken) { using (ApiTraceSource.Log.TraceMethod() .WithParameters(string.Join(",", permissions))) { var response = await this.service.Projects .TestIamPermissions( new TestIamPermissionsRequest() { Permissions = permissions.ToList() }, project.Name) .ExecuteAsync(cancellationToken) .ConfigureAwait(false); return response != null && response.Permissions != null && permissions.All(p => response.Permissions.Contains(p)); } } public async Task<OrganizationLocator?> FindOrganizationAsync( ProjectLocator project, CancellationToken cancellationToken) { using (ApiTraceSource.Log.TraceMethod().WithParameters(project)) { try { var response = await this.service.Projects .GetAncestry(new GetAncestryRequest(), project.Name) .ExecuteAsync(cancellationToken) .ConfigureAwait(false); // // The response contains all parent folders and the // organization. But if the user doesn't have sufficient // permission, this list can be incomplete or empty. // // NB. There's no way to know whether the project isn't part // of an organization or whether user lacks the permission // to access the organization. // var organization = response.Ancestor .EnsureNotNull() .FirstOrDefault(a => a.ResourceId?.Type == "organization"); if (organization != null && long.TryParse( organization.ResourceId.Id, out var id)) { return new OrganizationLocator(id); } else { return null; } } catch (GoogleApiException e) when (e.IsAccessDenied()) { throw new ResourceAccessDeniedException( $"You do not have sufficient permissions to access project {project.Name}. " + "You need the 'Compute Viewer' role (or an equivalent custom role) " + "to perform this action.", HelpTopics.ProjectAccessControl, e); } } } public async Task<Organization> GetOrganizationAsync( OrganizationLocator organization, CancellationToken cancellationToken) { using (ApiTraceSource.Log.TraceMethod().WithParameters(organization)) { try { return await this.service.Organizations .Get(organization.ToString()) .ExecuteAsync(cancellationToken) .ConfigureAwait(false); } catch (GoogleApiException e) when (e.IsAccessDenied()) { throw new ResourceAccessDeniedException( "You do not have sufficient permissions to access the " + $"organization {organization}. " + "You need the 'Organization Viewer' role (or an equivalent " + "custom role) to perform this action.", HelpTopics.ProjectAccessControl, e); } } } //--------------------------------------------------------------------- // IDisposable. //--------------------------------------------------------------------- public void Dispose() { this.service.Dispose(); } } public class FilteredProjectList { public IEnumerable<Project> Projects { get; } public bool IsTruncated { get; } public FilteredProjectList( IEnumerable<Project> projects, bool isTruncated) { this.Projects = projects; this.IsTruncated = isTruncated; } } public class ProjectFilter { private readonly string filter; private ProjectFilter(string filter) { this.filter = filter; } private static string Sanitize(string filter) { return filter .Replace(":", "") .Replace("\"", "") .Replace("'", ""); } /// <summary> /// Create filter for a specific project ID. /// </summary> public static ProjectFilter ByProjectId(string projectId) { projectId = Sanitize(projectId); return new ProjectFilter($"id:\"{projectId}\""); } /// <summary> /// Create filter for projects whose name or ID matches a term. /// </summary> public static ProjectFilter ByTerm(string term) { term = Sanitize(term); return new ProjectFilter($"name:\"*{term}*\" OR id:\"*{term}*\""); } public override string ToString() { return this.filter; } } }