C# OAA Application Connector

This document provides a high-level overview of and examples for getting started with a new OAA connector to integrate Veza with SaaS applications, infrastructure systems, custom-built applications, and other systems. These examples use C# and the Veza.OAA SDK.

When developing a connector, source system specifics and individual customer requirements will require alterations to code flow. However, the overall goals, best practices, and development flow are common to most integrations.

Code goals

The example code was written with the following goals in mind:

  • Connector should be easy to run from automation platforms and the Command Prompt.

  • Parameters are passed through environment variables as well as command line flags.

  • Connector does not require maintenance of state:

    • Connector does not require any persistent data between invocations.

    • There is no special invocation for the first execution.

    • The connector handles all provider and data source management logic.

    • Data source name is unique to the discovered environment.

High-level code flow

The exact flow of an OAA connector can change to meet specific requirements, but the general steps are as follows:

  1. Process and validate configuration parameters. Ensure that all required values are present and valid.

  2. Initialize the API client connection to the Veza tenant. Doing so early in the application flow validates the URL and API key before continuing discovery.

  3. Create an instance of the Veza.OAA.Application.CustomApplication class to populate with application metadata.

  4. Connect to the system and perform discovery of required entities.

    • In your custom integrations, discovery order for users, groups, roles, and other entities can adapt to suit application requirements.

    • Populate the CustomApplication instance with the identity, role, permission, resource, and authorization information collected.

  5. Check if the Provider and Data Source exist on Veza. Create them if they do not exist.

  6. Push the application to the Data Source on Veza. The SDK creates the required JSON payload from the CustomApplication instance.

    • Process any returned warnings or errors.

  7. Exit.

Customizing the example

To use this example as a starting point for your application integration, follow these steps:

  1. Update the name, applicationType, and description of the CustomApplication object based on the source system for integration with Veza.

  2. Define any custom_properties needed. Properties must be defined on the CustomApplication object before their values are set on any entities.

  3. Implement the discovery steps in the Discover() function to collect user, group, role, resource, and permission data for the application. As entities are collected, add them to the CustomApplication object.

  4. Run the connector to validate the output in Veza.

Example: custom application

The following code provides a template and examples for creating a new application integration using the Veza.OAA SDK.

example_app.cs
using RestSharp;
using RestSharp.Authenticators;
using Veza.OAA;
using Veza.OAA.Application;
using Veza.OAA.Client;
using NLog;

namespace Example.Integrations
{
    /// <summary>
    /// Example application group object
    /// </summary>
    public class AppGroup {
        public string Name { get; set; }
        public string Type { get; set; }
        public string CreatedAt { get; set; }
        public List<string> Users { get; set; }
    }

    /// <summary>
    /// Example application resource object
    /// </summary>
    public class AppResource {
        public string Name { get; set; }
        public int SiteId { get; set; }
        public string ResourceType { get; set; }
    }

    /// <summary>
    /// Example application role object
    /// </summary>
    public class AppRole {
        public string Name { get; set; }
        public int SystemId { get; set; }
        public string CreatedAt { get; set; }
        public List<string> Users { get; set; }
        public List<string> Resources { get; set; }
    }

    public class ExampleApp
    {
        // Optional NLog logger; can be replaced with any standard logging interface
        private static Logger logger = LogManager.GetCurrentClassLogger();

        // The base64-encoded logo for the application (displayed on the Veza UI)
        public string LogoBase64;

        // The name of the provider for the application
        public string ProviderName;

        // The custom application object for the example application
        // This object is used to build the application payload for Veza
        public CustomApplication CustomApp { get; set; }

        // The example application client for the example application API (not shown in this example)
        private ExampleAppClient AppClient { get; set; }

        # region "Construction"
        public ExampleApp()
        {
            // Instantiate the example application
            CustomApp = new CustomApplication(
                name: "Example Application 1",
                applicationType: "Example App",
                description: "Example application for Veza integration"
            );

            // Define an "uncategorized" permission for the example application
            // This can be used to group permissions that do not fit into another category,
            // or if the application API does not provide permission information
            CustomApp.AddCustomPermission(
                name: "Uncategorized",
                permissions: new List<Permission> { Permission.Uncategorized }
            );

            // define custom properties for the example application
            DefineCustomProperties();

            // Set the application icon for the Veza UI
            // This should be a base64-encoded SVG or PNG image
            LogoBase64 = "abc123...def987654"
        }

        /// <summary>
        /// Define custom properties for the example application.
        /// This method is called by the constructor to define any custom properties
        /// that the example application entities may have.
        /// </summary>
        private void DefineCustomProperties()
        {
            CustomApp.DefinedProperties[typeof(User)].
                DefineProperty("locked_out", typeof(bool);
            CustomApp.DefinedProperties[typeof(Group)].
                DefineProperty("type", typeof(string));
            CustomApp.DefinedProperties[typeof(Role)].
                DefineProperty("system_id", typeof(int));
            CustomApp.DefineResourceProperty("site_id", typeof(int)), "widget");
        }

        # endregion

        # region "Discovery"
        // Individual methods for discovering users, groups, roles, and resources.
        // Should be scoped as private to ensure that they are called only by the Discover method
        // and not directly by the client application.

        /// <summary>
        /// Discover users for the example application.
        /// This method is called by the Discover method to discover users for the example application.
        /// </summary>
        private void DiscoverUsers()
        {
            Logger.Info("Discovering users for the example application");

            // Get users from the example application client.
            // In this example, the GetUsers method returns a list of dictionaries of user data.
            List<Dictionary<string, string>> users = AppClient.GetUsers();

            // Iterate over the users response and add them to the application payload
            foreach (Dictionary<string, string> u in users)
            {
                // Create a new user object and add it to the application payload
                User user = CustomApp.AddUser(name: u["username"]);
                user.AddIdentity(u["email"]);
                user.IsActive = u["active"];
                user.CreatedAt = u["created_at"];
            }

            Logger.Info("User discovery complete");
        }

        /// <summary>
        /// Discover groups for the example application.
        /// This method is called by the Discover method to discover groups for the example application.
        /// </summary>
        private void DiscoverGroups()
        {
            Logger.Info("Discovering groups for the example application");

            // Get groups from the example application client
            // in this example, the GetGroups method returns a list of AppGroup objects
            List<AppGroup> groups = AppClient.GetGroups();

            // Iterate over the groups response and add them to the application payload
            foreach (AppGroup g in groups)
            {
                // Create a new group object and add it to the application payload
                Group group = CustomApp.AddGroup(name: g.Name);
                group.CreatedAt = g.CreatedAt;
                group.SetProperty(name: "type", value: g.Type);

                // Iterate users in the group and add them to the group
                foreach (string user in g.Users)
                {
                    CustomApp.Users[user].AddGroup(g.Name);
                }
            }

            Logger.Info("Group discovery complete");
        }

        /// <summary>
        /// Discover resources for the example application.
        /// This method is called by the Discover method to discover resources for the example application.
        /// </summary>
        private void DiscoverResources()
        {
            Logger.Info("Discovering resources for the example application");

            // Get resources from the example application
            // In this example, the GetResources method returns a list of AppResource objects
            List<AppResource> resources = AppClient.GetResources();
            foreach (AppResource r in resources)
            {
                // Create a resource object and add it to the application payload
                Resource resource = CustomApp.AddResource(name: r.Name, resourceType: r.ResourceType);

                // Set the `site_id` property for the resource if the resource is a "widget"
                // Cstom resource properties are defined per-resource type, so non-"widget" resources will not have this property
                if (r.ResourceType == "widget")
                {
                    resource.SetProperty(name: "site_id", value: r.SiteId);
                }
            }

            Logger.Info("Resource discovery complete");
        }

        /// <summary>
        /// Discover roles for the example application.
        /// This method is called by the Discover method to discover roles for the example application.
        /// </summary>
        private void DiscoverRoles()
        {
            Logger.Info("Discovering roles for the example application");

            // get roles from the example application
            // in this example, the GetRoles method returns a list of AppRole objects
            List<AppRole> roles = AppClient.GetRoles();
            foreach (Role role in roles)
            {
                // create a new role object and add it to the application payload
                Role role = CustomApp.AddRole(name: role.Name);
                role.CreatedAt = role.CreatedAt;
                role.SetProperty(name: "system_id", value: role.SystemId);

                // iterate users in the role and add them to the role
                // set the applyToApplication flag to true to apply the role to the entire application
                foreach (string user in role.Users)
                {
                    CustomApp.Users[user].AddRole(name: role.Name, applyToApplication: true);
                }
                Application.AddRole(role);
            }

            Logger.Info("Role discovery complete");
        }

        # endregion

        # region "Execution"

        /// <summary>
        /// Discover the example application.
        /// This method is called by the Run method to discover the example application.
        /// It serves as a place to instantiate a client for the example application API
        /// and then call each of the discovery methods required to build the application payload.
        /// </summary>
        /// <param name="exampleApiKey">The API key for the example application API</param>
        /// <param name="exampleUrl">The URL for the example application API</param>
        /// <returns></returns>
        public void Discover(string exampleApiKey, string exampleUrl)
        {
            // instantiate an API client for the example application
            AppClient = new ExampleAppClient(exampleApiKey, exampleUrl);

            Logger.Info("Beginning example application discovery");

            // discover the example application components
            DiscoverUsers();
            DiscoverGroups();
            DiscoverResources();
            DiscoverRoles();

            Logger.Info("Example application discovery complete");
        }

        /// <summary>
        /// Handle integration execution.
        /// This is separated from the Main method to more easily adapt to changes
        /// to application execution (e.g. from Command Prompt, as part of another application, etc.)
        /// </summary>
        /// <param name="exampleApiKey">The API key for the Example application API</param>
        /// <param name="exampleUrl">The URL for the Example application API</param>
        /// <param name="vezaApiKey">The API key for the Veza tenant</param>
        /// <param name="vezaUrl">The URL for the Veza tenant</param>
        /// <returns></returns>
        public async Task Run(
            string exampleApiKey,
            string exampleUrl,
            string vezaApiKey,
            string vezaUrl
        )
        {
            Logger.Info("Starting example application discovery");

            // Instantiate a connection to the Veza API.
            // The client will attempt to reach the Veza API and throw an exception if it fails.
            // This is to ensure that the Veza API is reachable before attempting to discover and push data.
            try
            {
                OAAClient oaaClient = new(vezaApiKey, vezaUrl);
            }
            catch (ClientException e)
            {
                Logger.Error("Failed to connect to Veza API: {0}", e.Message);
                return;
            }

            // Discover the example application
            Discover(exampleApiKey, exampleUrl);

            // Get or create the Veza provider for the payload
            Logger.Debug("Checking Veza for an existing provider for the example application");
            Sdk.Client.ApiClient.VezaApiResponse providerResponse = await oaaClient.GetProvider(ProviderName);

            // Create the provider if none exists
            if (providerResponse is null)
            {
                Logger.Info("No Veza provider exists; creating provider");
                await oaaClient.CreateProvider(ProviderName, "application", base64_icon: LogoBase64);
            }

            // Push OAA payload to Veza
            Logger.Info("Pushing example application metadata to Veza");
            RestResponse<Sdk.Client.ApiClient.VezaApiResponse> pushResponse = await oaaClient.PushApplication(ProviderName, Environment.MachineName, CustomApp, save_json: true);
            if (pushResponse.IsSuccessful)
            {
                Logger.Info("Example application discovery complete");
            } else
            {
                Logger.Error($"Push response code: {pushResponse.StatusCode}");
                Logger.Error($"Push response content: {pushResponse.Content}");
            }
        }

        # endregion
    }
}

Last updated