/// <summary> /// Get a scope description string. /// /// Group the categories together so as to generate the most minimal string. Some of this is done via /// the flags enum minimization step, but there are some disjoint roots that require an additional step. /// <param name="scopes">Scopes</param> /// <returns>Description string.</returns> /// <exception cref="Exception">Exception if an attribute is missing from the enum.</exception> public static string GetScopeString(this AzureDevOpsPATScopes scopes) { var minimalScopes = scopes.GetMinimizedScopeList(); var azdoScopeType = typeof(AzureDevOpsPATScopes); var shortHandType = typeof(ScopeDescriptionAttribute); Dictionary <string, string> minimalScopesStrings = new Dictionary <string, string>(); // Build up a dictionary of strings with common category roots. foreach (var scope in minimalScopes) { var memberInfo = azdoScopeType.GetMember(scope.ToString()); var attribute = Attribute.GetCustomAttribute(memberInfo[0], shortHandType); if (attribute == null) { throw new Exception($"{scope.ToString()} should have a 'ScopeShortHand' attribute."); } var shortHandTypeAttribute = (ScopeDescriptionAttribute)attribute; if (minimalScopesStrings.ContainsKey(shortHandTypeAttribute.Resource)) { minimalScopesStrings[shortHandTypeAttribute.Resource] += shortHandTypeAttribute.Permissions; } else { minimalScopesStrings.Add(shortHandTypeAttribute.Resource, shortHandTypeAttribute.Permissions); } } // Join their values together. return(string.Join('-', minimalScopesStrings.Select(kv => $"{kv.Key}-{kv.Value}"))); }
/// <summary> /// Pull out a minimal /// </summary> /// <param name="e"></param> /// <returns></returns> public static List <AzureDevOpsPATScopes> GetMinimizedScopeList(this AzureDevOpsPATScopes e) { // Note that in net5.0+ there is a generic version of this, but // it's not available in 3.1. // Get the list of available values, which are automatically sorted by their magnitude. var availableValues = (int[])Enum.GetValues(typeof(AzureDevOpsPATScopes)); // To figure out a minimal set, rely on the flags setup that means that more permissive scopes // include the less permissive ones. // // We walk the list of avaiable values, checking whether the bit is set. If it is, check whether // any existing scopes in the list are contained within this scope. If they are, remove them. // Then add the new scope to the list. List <AzureDevOpsPATScopes> minimalScopes = new List <AzureDevOpsPATScopes>(); foreach (var value in availableValues) { AzureDevOpsPATScopes flag = (AzureDevOpsPATScopes)value; if (e.HasFlag(flag)) { // If this scope includes any of the existing minimal scopes (X & Y != 0) then // remove those existing minimal scopes. var existingMatchingMinimalScopes = minimalScopes.Where(ms => (flag & ms) != 0).ToList(); if (existingMatchingMinimalScopes.Any()) { existingMatchingMinimalScopes.ForEach(ms => minimalScopes.Remove(ms)); } minimalScopes.Add(flag); } } return(minimalScopes); }
/// <summary> /// Determine the desired name of the PAT. It appears that PATs may have really any combination of characters /// and they do not have to be unique. /// </summary> /// <param name="scopes">Desired scopes</param> /// <param name="organizations">Organizations</param> /// <param name="name">Name provided by the user</param> /// <returns>Name of the PAT.</returns> private static string GetPatName(AzureDevOpsPATScopes scopes, string[] organizations, string name) { string patName = name; if (string.IsNullOrEmpty(patName)) { string scopeString = scopes.GetScopeString(); patName = $"{string.Join("-", organizations)}-{scopeString}"; } return(patName); }
private static async Task <int> Go(List <AzureDevOpsPATScopes> scopes, string[] organizations, string name, int?expiresIn, DateTime?expiration, string user, string password, IConsole console) { AzureDevOpsPATScopes scopeFlags = 0; foreach (var scope in scopes) { scopeFlags |= scope; } string patName = GetPatName(scopeFlags, organizations, name); if (expiresIn.HasValue && expiration.HasValue) { Console.WriteLine("May not specify both --expires-in and --expiration."); return(1); } if (string.IsNullOrEmpty(user) != string.IsNullOrEmpty(password)) { Console.WriteLine("Must specify both user + password, or neither."); return(1); } DateTime credentialExpiration = GetExpirationDate(expiration, expiresIn); VssCredentials credentials; if (!string.IsNullOrEmpty(user)) { credentials = new VssAadCredential(user, password); } else { credentials = await GetInteractiveUserCredentials(); } var patGenerator = new AzureDevOpsPATGenerator(credentials); var pat = await patGenerator.GeneratePATAsync(patName, scopeFlags, organizations, credentialExpiration); Console.WriteLine($"{patName} (Valid Until: {credentialExpiration}): {pat.Token}"); return(0); }
/// <summary> /// Generate an azure devops PAT with a given name, target organization set, and scopes. /// </summary> /// <param name="name"></param> /// <param name="targetScopes"></param> /// <param name="targetOrganizationNames"></param> /// <param name="validTo"></param> /// <returns>New PAT</returns> /// <exception cref="ArgumentException"></exception> /// <exception cref="Exception"></exception> public async Task <SessionToken> GeneratePATAsync( string name, AzureDevOpsPATScopes targetScopes, IEnumerable <string> targetOrganizationNames, DateTime validTo) { ValidateParameters(name, targetScopes, targetOrganizationNames, validTo); var minimalScopesList = targetScopes.GetMinimizedScopeList(); var scopesWithPrefixes = minimalScopesList.Select(scope => $"vso.{scope}"); string scopes = string.Join(" ", scopesWithPrefixes); try { using var tokenConnection = new VssConnection( new Uri("https://vssps.dev.azure.com/"), credentials); using var tokenClient = await tokenConnection.GetClientAsync <TokenHttpClient>(); var organizations = await GetAccountsByNameAsync(targetOrganizationNames); var tokenInfo = new SessionToken() { DisplayName = name, Scope = scopes, TargetAccounts = organizations.Select(account => account.AccountId).ToArray(), ValidFrom = DateTime.UtcNow, ValidTo = validTo, }; SessionToken pat = await tokenClient.CreateSessionTokenAsync( tokenInfo, SessionTokenType.Compact, isPublic : false); return(pat); } catch (Exception ex) { throw new Exception($"Failed to generate a PAT named '{name}' for organizatons '{string.Join(", ", targetOrganizationNames)}' and scopes '{scopes}'", ex); } }
private static void ValidateParameters(string name, AzureDevOpsPATScopes targetScopes, IEnumerable <string> targetOrganizationNames, DateTime validTo) { if (string.IsNullOrEmpty(name)) { throw new ArgumentException("PAT must have a name"); } if (targetScopes == 0) { throw new ArgumentException("PAT must have one or more desired scopes."); } if (!targetOrganizationNames.Any()) { throw new ArgumentException("PAT must have one or more orgnanizations."); } if (validTo.CompareTo(DateTime.Now) < 0) { throw new ArgumentException("PAT expiration date must be after the current date."); } }
public void MinimalScopeStringTests(AzureDevOpsPATScopes scopes, string expectedString) { scopes.GetScopeString().Should().Be(expectedString); }