I recently had a need to work with both a Secure Resource and Secure Credential from within an Otter script, but found it was difficult to determine what property names were available when using $SecureCredentialProperty(...) and $SecureResourceProperty(...), as they were not fully-documented and are dynamically generated based on the resource/credential type (which may themselves be provided by plugins).
To compensate, I knocked up a couple of variable functions in an scratch extension of my own. I figured they might be useful for others, and so am providing them below.
Long term, they are possibly best-served living in OtterEx.dll, alongside the existing $SecureCredentialProperty function, but I don't think that it available via your GitHub for pull-requests (I had to run OtterEx.dll through a decompiler to figure out what was needed).
In any case, if they are of any use to you, you are more than welcome to them...
SecureResourcePropertiesVariableFunction.cs
using System.Collections;
using System.ComponentModel;
using System.Reflection;
using Inedo.ExecutionEngine.Executer;
using Inedo.Extensibility;
using Inedo.Extensibility.Credentials;
using Inedo.Extensibility.VariableFunctions;
using Inedo.Serialization;
using SecureCreds = Inedo.Extensibility.Credentials.SecureCredentials;
namespace Inedo.Extensions.VariableFunctions.SecureCredentials
{
[ScriptAlias("SecureCredentialProperties")]
[Description("Gets the properties available to a named Secure Credential. Use `$SecureCredentialProperty()` to obtain the value.")]
public sealed class SecureCredentialPropertiesVariableFunction : VectorVariableFunction
{
[VariableFunctionParameter(0)]
[ScriptAlias("credential")]
[Description("The name of the credential for which to fetch property names")]
public string? CredentialName { get; set; }
protected override IEnumerable? EvaluateVector(IVariableFunctionContext context)
=> GetProperties(CredentialName, context).Select(p => p.Name);
internal static IEnumerable<PropertyInfo> GetProperties(string? credentialName, IVariableFunctionContext context)
{
var credentialResolutionContext = new CredentialResolutionContext(context.ProjectId, context.EnvironmentId);
var secureCredential = SecureCreds.TryCreate(credentialName, credentialResolutionContext)
?? throw new ExecutionFailureException(string.Concat(
"Could not find a Secure Credential named \"",
credentialName,
"\"; this error may occur if you renamed a credential, " +
"or the application or environment in context does not " +
"match any existing credentials. To resolve, edit this " +
"item, property, or operation's configuration, ensure a " +
"valid credential for the application/environment in " +
"context is selected, and then save."));
return Persistence.GetPersistentProperties(secureCredential.GetType(), true);
}
}
[ScriptAlias("SecureCredentialHasProperty")]
[Description("Checks if the named property can be read from the named Secure Credential")]
public sealed class SecureCredentialHasPropertyVariableFunction : ScalarVariableFunction
{
[VariableFunctionParameter(0)]
[ScriptAlias("credential")]
[Description("The name of the Secure Credential to test.")]
public string? CredentialName { get; set; }
[Description("The name of the property to test.")]
[ScriptAlias("property")]
[VariableFunctionParameter(1)]
public string? PropertyName { get; set; }
[Description("Set to `false` to return `false` if the property is found but encrypted " +
"and/or requires additional script access permissions. Set to `true` " +
"(i.e. the default) to test only whether the property exists.")]
[ScriptAlias("whenEncrypted")]
[VariableFunctionParameter(2, Optional=true)]
public bool WhenEncrypted { get; set; } = true;
protected override object? EvaluateScalar(IVariableFunctionContext context)
{
if (string.IsNullOrWhiteSpace(PropertyName)) return false;
var prop = SecureCredentialPropertiesVariableFunction
.GetProperties(CredentialName, context)
.FirstOrDefault(p => string.Equals(p.Name, PropertyName, StringComparison.InvariantCultureIgnoreCase));
if (prop != null)
{
if (WhenEncrypted == false)
{
var encrypted = prop.GetCustomAttribute<PersistentAttribute>()?.Encrypted ?? false;
if (encrypted) return false;
}
return true;
}
return false;
}
}
}
SecureCredentialPropertiesVariableFunction.cs
using System.Collections;
using System.ComponentModel;
using System.Reflection;
using Inedo.ExecutionEngine.Executer;
using Inedo.Extensibility;
using Inedo.Extensibility.Credentials;
using Inedo.Extensibility.VariableFunctions;
using Inedo.Serialization;
using SecureCreds = Inedo.Extensibility.Credentials.SecureCredentials;
namespace Inedo.Extensions.VariableFunctions.SecureCredentials
{
[ScriptAlias("SecureCredentialProperties")]
[Description("Gets the properties available to a named Secure Credential. Use `$SecureCredentialProperty()` to obtain the value.")]
public sealed class SecureCredentialPropertiesVariableFunction : VectorVariableFunction
{
[VariableFunctionParameter(0)]
[ScriptAlias("credential")]
[Description("The name of the credential for which to fetch property names")]
public string? CredentialName { get; set; }
protected override IEnumerable? EvaluateVector(IVariableFunctionContext context)
=> GetProperties(CredentialName, context).Select(p => p.Name);
internal static IEnumerable<PropertyInfo> GetProperties(string? credentialName, IVariableFunctionContext context)
{
var credentialResolutionContext = new CredentialResolutionContext(context.ProjectId, context.EnvironmentId);
var secureCredential = SecureCreds.TryCreate(credentialName, credentialResolutionContext)
?? throw new ExecutionFailureException(string.Concat(
"Could not find a Secure Credential named \"",
credentialName,
"\"; this error may occur if you renamed a credential, " +
"or the application or environment in context does not " +
"match any existing credentials. To resolve, edit this " +
"item, property, or operation's configuration, ensure a " +
"valid credential for the application/environment in " +
"context is selected, and then save."));
return Persistence.GetPersistentProperties(secureCredential.GetType(), true);
}
}
[ScriptAlias("SecureCredentialHasProperty")]
[Description("Checks if the named property can be read from the named Secure Credential")]
public sealed class SecureCredentialHasPropertyVariableFunction : ScalarVariableFunction
{
[VariableFunctionParameter(0)]
[ScriptAlias("credential")]
[Description("The name of the Secure Credential to test.")]
public string? CredentialName { get; set; }
[Description("The name of the property to test.")]
[ScriptAlias("property")]
[VariableFunctionParameter(1)]
public string? PropertyName { get; set; }
[Description("Set to `false` to return `false` if the property is found but encrypted " +
"and/or requires additional script access permissions. Set to `true` " +
"(i.e. the default) to test only whether the property exists.")]
[ScriptAlias("whenEncrypted")]
[VariableFunctionParameter(2, Optional=true)]
public bool WhenEncrypted { get; set; } = true;
protected override object? EvaluateScalar(IVariableFunctionContext context)
{
if (string.IsNullOrWhiteSpace(PropertyName)) return false;
var prop = SecureCredentialPropertiesVariableFunction
.GetProperties(CredentialName, context)
.FirstOrDefault(p => string.Equals(p.Name, PropertyName, StringComparison.InvariantCultureIgnoreCase));
if (prop != null)
{
if (WhenEncrypted == false)
{
var encrypted = prop.GetCustomAttribute<PersistentAttribute>()?.Encrypted ?? false;
// TODO: this should probably actually check if script access has been granted, but
// this is buried in Inedo.Otter.Data.Tables.Credentials_Extended, and I'm not sure
// if that is safe to wrap in an Extension
// For now, we just determine if it would be encrypted, not whether we can access
if (encrypted) return false;
}
return true;
}
return false;
}
}
}

)
(play) button icon against each execution, for which I believe the intention is to re-run any one particular execution with the variable prompts pre-populated with the previously-entered values for that execution.
