# Implementation Plan: `get_license` REST API Action

## Overview

This document describes the implementation plan for a new REST API action called `get_license` that returns license information in JSON format. 

### Key Requirements
- **JSON Type**: `get_license`
- **Authentication**: Requires a valid API Key Id and API Key pair (like most API calls)
- **Authorization**: Does NOT require specific access rights (unlike `find_user` which requires `ApiPermissionsEnum.FindUser`)
- **Response**: Returns V2 JSON license data if available, or creates a minimum JSON response for V1 licenses

---

## Architecture Overview

The REST API is controlled by API Actions. Each API action:
1. Implements `IApiAction` interface
2. Is registered in `ApiActionFactory.cs`
3. Has a type constant in `server_response` class in `RestApiProcessor.cs`
4. Handles its own authentication/authorization

### Existing Patterns

**Actions requiring authentication AND specific permissions** (e.g., `FindUser.cs`):
```csharp
var tPartner = authServerApi.IsApiRequestAuthorized(conn,
                                                    partner.AuthServerLoginName,
                                                    partner.AuthServerLoginPassword,
                                                    ApiPermissionsEnum.FindUser,  // <-- Requires specific permission
                                                    AuditAction.FindUser);
```

**Actions NOT requiring authentication** (e.g., `GetSystemTime.cs`):
- No API credentials validation at all
- Returns system time to any caller
- No database connection needed

**Actions requiring authentication but NO specific permissions**:
- This is the pattern `get_license` will follow
- Validates API Key credentials are valid
- Uses a new `GetLicense` permission that is auto-granted after API key validation
- The `CheckAPIPermission` method will check for `GetLicense` permission right after validating the API key, before checking `AccessRights`

---

## Files to Create

### 1. Response Model
**File**: `SurePassIdLegacyMfaServer\server\Models\GetLicenseResponse.cs`

```csharp
using Newtonsoft.Json;
using SurePassAdmin.Fido;

namespace SurePassAdmin.server.Models
{
    /// <summary>
    /// Response model for the get_license API action.
    /// Returns license information in JSON format.
    /// Supports both V1 (legacy) and V2 (JSON) license formats.
    /// </summary>
    public class GetLicenseResponse : server_response
    {
        // Basic license properties (V1 & V2)
        [JsonProperty("licenseVersion")]
        public int LicenseVersion { get; set; }

        [JsonProperty("licenseStatus")]
        public string LicenseStatus { get; set; }

        [JsonProperty("expirationDate")]
        public string ExpirationDate { get; set; }

        [JsonProperty("hasExpirationDate")]
        public bool HasExpirationDate { get; set; }

        [JsonProperty("maxUsers")]
        public int MaxUsers { get; set; }

        [JsonProperty("hostedInstall")]
        public bool HostedInstall { get; set; }

        [JsonProperty("communityEdition")]
        public bool CommunityEdition { get; set; }

        [JsonProperty("evaluation")]
        public bool Evaluation { get; set; }

        [JsonProperty("evaluationExpired")]
        public bool EvaluationExpired { get; set; }

        [JsonProperty("daysUsed")]
        public short DaysUsed { get; set; }

        [JsonProperty("maxDays")]
        public short MaxDays { get; set; }

        // V2 only - nullable for V1 licenses
        [JsonProperty("product", NullValueHandling = NullValueHandling.Ignore)]
        public LicenseProductInfo Product { get; set; }

        [JsonProperty("company", NullValueHandling = NullValueHandling.Ignore)]
        public LicenseCompanyInfo Company { get; set; }

        [JsonProperty("licensing", NullValueHandling = NullValueHandling.Ignore)]
        public LicenseLicensingInfo Licensing { get; set; }

        public GetLicenseResponse(string type) : base(type) { }
    }

    /// <summary>
    /// Product information from V2 license.
    /// </summary>
    public class LicenseProductInfo
    {
        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("edition")]
        public string Edition { get; set; }

        [JsonProperty("version")]
        public string Version { get; set; }

        [JsonProperty("maxTokens")]
        public int MaxTokens { get; set; }

        [JsonProperty("maxUsers")]
        public int MaxUsers { get; set; }

        [JsonProperty("multiTenantInstall")]
        public bool MultiTenantInstall { get; set; }

        [JsonProperty("componentList")]
        public string[] ComponentList { get; set; }

        [JsonProperty("platformList")]
        public string[] PlatformList { get; set; }
    }

    /// <summary>
    /// Company information from V2 license.
    /// </summary>
    public class LicenseCompanyInfo
    {
        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("contact")]
        public string Contact { get; set; }

        [JsonProperty("email")]
        public string Email { get; set; }
    }

    /// <summary>
    /// Licensing metadata from V2 license.
    /// </summary>
    public class LicenseLicensingInfo
    {
        [JsonProperty("issueDate")]
        public string IssueDate { get; set; }

        [JsonProperty("licenseKey")]
        public string LicenseKey { get; set; }

        [JsonProperty("bypassHostNameValidation")]
        public bool BypassHostNameValidation { get; set; }
    }
}
```

### 2. API Action
**File**: `SurePassIdLegacyMfaServer\server\ApiActions\GetLicense.cs`

```csharp
using Newtonsoft.Json.Linq;
using SurePassAdmin.Fido;
using SurePassAdmin.server.Models;
using SurePassClassLib;
using SurePassClassLib.Constants;
using SurePassClassLib.Database;
using SurePassClassLib.Enums;
using System;

namespace SurePassAdmin.server.ApiActions
{
    /// <summary>
    /// API Action: get_license
    /// Returns license information in JSON format.
    /// 
    /// Authentication: Requires valid API Key Id and API Key pair
    /// Authorization: Uses GetLicense permission - automatically granted after API key validation
    /// </summary>
    public class GetLicense : ApiAction, IApiAction
    {
        public bool Applies(string type)
        {
            return type.Equals(server_response.GET_LICENSE, StringComparison.OrdinalIgnoreCase);
        }

        public object Execute(dynamic json, string username, spCommonPage sp, 
                             TablePartner partner, bool trace, string serverAppId)
        {
            object response;
            var ipAddressData = sp.GetIpAddressData();
            var ipAddress = ipAddressData.ToString();

            try
            {
                string requestData = json.ToString();
                WriteTraceRequestJson(username, requestData, ipAddress, partner);

                var conn = new Connection();

                try
                {
                    if (conn.Connect())
                    {
                        conn.OpenConnection();

                        var authServerApi = new AuthServerApi
                        {
                            ApiRequestData = FormatJson(requestData),
                            IpAddressData = ipAddressData
                        };

                        // Use the new GetLicense permission - this permission is automatically 
                        // granted after API key validation (handled in CheckAPIPermission)
                        var tPartner = authServerApi.IsApiRequestAuthorized(conn,
                                                                            partner.AuthServerLoginName,
                                                                            partner.AuthServerLoginPassword,
                                                                            ApiPermissionsEnum.GetLicense,
                                                                            AuditAction.ServerConfiguration);

                        if (tPartner == null)
                        {
                            WriteTraceRequestError(username, 
                                $"{server_response.GET_LICENSE}: Api Error={authServerApi.ErrorMsg} Code={authServerApi.ErrorCode}", 
                                ipAddress, partner);
                            return FormatError(server_response.GET_LICENSE, 
                                              authServerApi.ErrorMsg, 
                                              AuditResultCode.Find(authServerApi.ErrorCode.Code), 
                                              trace);
                        }

                        // Read license information
                        var licenseInfo = WebUtil.GetLicenseFile();

                        if (!string.IsNullOrEmpty(licenseInfo.LastError))
                        {
                            WriteTraceRequestError(username, 
                                $"{server_response.GET_LICENSE}: License Error={licenseInfo.LastError}", 
                                ipAddress, partner);
                            return FormatError(server_response.GET_LICENSE, 
                                              licenseInfo.LastError, 
                                              AuditResultCode.DiagnosticTrace, 
                                              trace);
                        }

                        // Build response
                        response = BuildLicenseResponse(licenseInfo);
                    }
                    else
                    {
                        WriteTraceRequestError(username, 
                            $"{server_response.GET_LICENSE}: Error: {conn.GetLastError()}", 
                            ipAddress, partner);
                        response = FormatError(server_response.GET_LICENSE, 
                                              conn.GetLastError(), 
                                              AuditResultCode.DiagnosticTrace, 
                                              trace);
                    }
                }
                catch (Exception ex2)
                {
                    WriteTraceRequestError(username, 
                        $"{server_response.GET_LICENSE}: Error: {ex2.Message}", 
                        ipAddress, partner);
                    response = FormatError(server_response.GET_LICENSE, 
                                          ex2.Message, 
                                          AuditResultCode.DiagnosticTrace, 
                                          trace);
                }
                finally
                {
                    conn.CloseConnection();
                }
            }
            catch (Exception ex)
            {
                WriteTraceRequestError(username, 
                    $"{server_response.GET_LICENSE}: Error: {ex.Message}", 
                    ipAddress, partner);
                response = FormatError(server_response.GET_LICENSE, 
                                      ex.Message, 
                                      AuditResultCode.DiagnosticTrace, 
                                      trace);
            }

            WriteTraceResponseJson(username, ((JObject)JToken.FromObject(response)).ToString(), ipAddress, partner);
            return response;
        }

        /// <summary>
        /// Builds the license response based on license version.
        /// V1 licenses get basic fields only.
        /// V2 licenses include full Product/Company/Licensing data.
        /// </summary>
        private GetLicenseResponse BuildLicenseResponse(MfaServerLicense licenseInfo)
        {
            var response = new GetLicenseResponse(server_response.GET_LICENSE)
            {
                // Basic fields (V1 & V2)
                LicenseVersion = licenseInfo.LicenseVersion,
                LicenseStatus = licenseInfo.LicenseStatus.ToString(),
                ExpirationDate = licenseInfo.ExpirationDate.ToString("O"), // ISO 8601
                HasExpirationDate = licenseInfo.HasExpirationDate,
                MaxUsers = licenseInfo.MaxUsers,
                HostedInstall = licenseInfo.HostedInstall,
                CommunityEdition = licenseInfo.CommunityEdition,
                Evaluation = licenseInfo.Evaluation,
                EvaluationExpired = licenseInfo.EvaluationExpired,
                DaysUsed = licenseInfo.DaysUsed,
                MaxDays = licenseInfo.MaxDays
            };

            // V2 license - include extended data
            if (licenseInfo.LicenseVersion == 2 && licenseInfo.LicenseData != null)
            {
                // Product info
                if (licenseInfo.LicenseData.Product != null)
                {
                    response.Product = new LicenseProductInfo
                    {
                        Name = licenseInfo.LicenseData.Product.Name,
                        Edition = licenseInfo.LicenseData.Product.Edition,
                        Version = licenseInfo.LicenseData.Product.Version,
                        MaxTokens = licenseInfo.LicenseData.Product.MaxTokens,
                        MaxUsers = licenseInfo.LicenseData.Product.MaxUsers,
                        MultiTenantInstall = licenseInfo.LicenseData.Product.MultiTenantInstall,
                        ComponentList = licenseInfo.LicenseData.Product.ComponentList,
                        PlatformList = licenseInfo.LicenseData.Product.PlatformList
                    };
                }

                // Company info
                if (licenseInfo.LicenseData.Company != null)
                {
                    response.Company = new LicenseCompanyInfo
                    {
                        Name = licenseInfo.LicenseData.Company.Name,
                        Contact = licenseInfo.LicenseData.Company.Contact,
                        Email = licenseInfo.LicenseData.Company.Email
                    };
                }

                // Licensing info
                if (licenseInfo.LicenseData.Licensing != null)
                {
                    response.Licensing = new LicenseLicensingInfo
                    {
                        IssueDate = licenseInfo.LicenseData.Licensing.IssueDate.ToString("O"),
                        LicenseKey = licenseInfo.LicenseData.Licensing.LicenseKey,
                        BypassHostNameValidation = licenseInfo.LicenseData.Licensing.BypassHostNameValidation
                    };
                }
            }

            return response;
        }
    }
}
```

---

## Files to Modify

### 3. Add GetLicense Permission to Enum
**File**: `Submodules\SurePassIdDotNetLibs\SurePassClassLib\Enums\ApiPermissionsEnum.cs`

Add `GetLicense` to the enum (after `Fido2Delete = 52`):

```csharp
Fido2Delete,
GetLicense  // = 53 - Special permission: auto-granted after API key validation
```

### 4. Modify CheckAPIPermission to Auto-Grant GetLicense
**File**: `Submodules\SurePassIdDotNetLibs\SurePassClassLib\AuthServerAPI\AuthServerAPIAccessKeyCheck.cs`

In the `CheckAPIPermission` method, after `FindPartnerAccessKeyByAccessLogin` succeeds (after the API key is validated), add a check to auto-grant `GetLicense` permission:

```csharp
if (partnerApi.FindPartnerAccessKeyByAccessLogin(conn,
                                                 authServerPartnerLoginName,
                                                 authServerPartnerLoginPassword))
{
    var failedPermissionList = string.Empty;

    ApiKeyName = partnerApi.Description.ToString();

    // Auto-grant GetLicense permission - any valid API key can retrieve license info
    if (requestedPermissions.Length == 1 && requestedPermissions[0] == ApiPermissionsEnum.GetLicense)
    {
        partnerApi.UpdateLastAccess(conn, IpAddressData.ToString());

        if (SurePassConfiguration.GetConfigParameterBool("System.LogApiKeyPermissionSuccess", false))
        {
            ErrorMsg = GetSystemMessage("ASAPI_PARTNER_API_LOGIN_SUCCESS").Replace("%1", authServerPartnerLoginName)
                                                                          .Replace("%2", ApiKeyName);
            ErrorCode = AuditResultCode.Success;

            WriteAuditRecord(AuditAction.ApiAccessCheck,
                             AuditResultCode.Success,
                             AuditSeverity.Success,
                             AuditType.ServerAuth,
                             ErrorMsg,
                             partnerApi.PartnerId);
        }

        var interfacePartner = new InterfacePartner();
        var partner = interfacePartner.GetPartner(conn, partnerApi.PartnerId);

        if (partner == null)
        {
            ErrorMsg = GetSystemMessage("ASAPI_PARTNER_API_LOGIN_FAILURE").Replace("%1", $"Id={partnerApi.PartnerId} {authServerPartnerLoginName}")
                                                                          .Replace("%2", "GetLicense")
                                                                          .Replace("%3", "Partner not found");

            ErrorCode = AuditResultCode.ApiAccessViolation;

            WriteAuditRecord(AuditAction.ApiAccessCheck,
                            AuditResultCode.ApiAccessViolation,
                            AuditSeverity.Severe,
                            AuditType.ServerAuth,
                            ErrorMsg,
                            partnerApi.PartnerId);

            return null;
        }

        return partner;
    }

    // Continue with normal permission checking...
    foreach (var requestPermission in requestedPermissions)
    {
        // ... existing code ...
    }
```

### 5. Add Type Constant
**File**: `SurePassIdLegacyMfaServer\server\RestApiProcessor.cs`

### 5. Add Type Constant
**File**: `SurePassIdLegacyMfaServer\server\RestApiProcessor.cs`

Add to `server_response` class (around line 200, with other static properties):

```csharp
public static string GET_LICENSE { get; } = "get_license";
```

### 6. Register Action in Factory
**File**: `SurePassIdLegacyMfaServer\server\ApiActions\ApiActionFactory.cs`

Add to constructor (alphabetically with other actions, around line 24):

```csharp
_actions.Add(new GetLicense());
```

### 7. Update RestApiProcessor to Allow API Key Check (Optional)

If you want the `get_license` API to require API credentials like other protected endpoints, you may need to add `server_response.GET_LICENSE` to the exclusion list in `ProcessRequest()` method if it should bypass the standard API key validation flow.

Looking at the existing code in `RestApiProcessor.cs`, the following endpoints bypass API key validation:
- `PROVISION_OATH_DEVICE`
- `PROVISION_DEVICE`
- `PROVISION_PUSH_DEVICE`
- `DELETE_PUSH_USER_DEVICE`
- `UPDATE_PUSH_USER_DEVICE`
- `GET_SYSTEM_TIME`

Since `get_license` requires API key authentication, it should **NOT** be added to this list - the standard flow will validate credentials.

---

## API Request/Response Specification

### Request Format

```json
{
    "type": "get_license",
    "spAccountLoginName": "<API_KEY_ID>",
    "spAccountLoginKey": "<API_KEY>"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | Must be `"get_license"` |
| `spAccountLoginName` | string | Yes | API Key ID |
| `spAccountLoginKey` | string | Yes | API Key Secret |

### Response Format (V2 License)

```json
{
    "type": "get_license",
    "errorCode": 0,
    "errorMessage": "OK",
    "licenseVersion": 2,
    "licenseStatus": "Valid",
    "expirationDate": "2025-12-31T00:00:00.0000000",
    "hasExpirationDate": true,
    "maxUsers": 1000,
    "hostedInstall": false,
    "communityEdition": false,
    "evaluation": false,
    "evaluationExpired": false,
    "daysUsed": 0,
    "maxDays": 0,
    "product": {
        "name": "SurePassID Authentication Services",
        "edition": "Enterprise",
        "version": "2025.4",
        "maxTokens": 5000,
        "maxUsers": 1000,
        "multiTenantInstall": false,
        "componentList": ["Mfa Server", "Radius", "Saml2 IdP"],
        "platformList": ["On-Premises"]
    },
    "company": {
        "name": "Acme Corporation",
        "contact": "John Doe",
        "email": "john.doe@acme.com"
    },
    "licensing": {
        "issueDate": "2025-01-01T00:00:00.0000000",
        "licenseKey": "SPID-2025-ENT-ACME-001",
        "bypassHostNameValidation": false
    }
}
```

### Response Format (V1 Legacy License)

```json
{
    "type": "get_license",
    "errorCode": 0,
    "errorMessage": "OK",
    "licenseVersion": 1,
    "licenseStatus": "Valid",
    "expirationDate": "2025-12-31T00:00:00.0000000",
    "hasExpirationDate": true,
    "maxUsers": 100,
    "hostedInstall": false,
    "communityEdition": false,
    "evaluation": false,
    "evaluationExpired": false,
    "daysUsed": 0,
    "maxDays": 0
}
```

> **Note**: V1 licenses do not include `product`, `company`, or `licensing` objects.

### Error Response

```json
{
    "type": "get_license",
    "errorCode": 9131,
    "errorMessage": "Api Access Violation"
}
```

---

## Response Field Reference

| Field | Type | V1 | V2 | Description |
|-------|------|----|----|-------------|
| `type` | string | ? | ? | Always `"get_license"` |
| `errorCode` | int | ? | ? | 0 = success |
| `errorMessage` | string | ? | ? | "OK" or error message |
| `licenseVersion` | int | ? | ? | 1 or 2 |
| `licenseStatus` | string | ? | ? | "Valid", "Expired", etc. |
| `expirationDate` | string | ? | ? | ISO 8601 format |
| `hasExpirationDate` | bool | ? | ? | Whether license expires |
| `maxUsers` | int | ? | ? | Maximum licensed users |
| `hostedInstall` | bool | ? | ? | Multi-tenant mode |
| `communityEdition` | bool | ? | ? | Community edition flag |
| `evaluation` | bool | ? | ? | Evaluation license flag |
| `evaluationExpired` | bool | ? | ? | Evaluation period expired |
| `daysUsed` | short | ? | ? | Days used in evaluation |
| `maxDays` | short | ? | ? | Max evaluation days |
| `product` | object | ? | ? | Product details |
| `company` | object | ? | ? | Company details |
| `licensing` | object | ? | ? | Licensing metadata |

---

## Implementation Checklist

### Phase 1: Add GetLicense Permission
- [ ] Add `GetLicense = 53` to `ApiPermissionsEnum.cs`
- [ ] Modify `CheckAPIPermission` in `AuthServerAPIAccessKeyCheck.cs` to auto-grant GetLicense

### Phase 2: Create New Files
- [ ] Create `SurePassIdLegacyMfaServer\server\Models\GetLicenseResponse.cs`
- [ ] Create `SurePassIdLegacyMfaServer\server\ApiActions\GetLicense.cs`

### Phase 3: Modify Existing Files
- [ ] Add `GET_LICENSE` constant to `RestApiProcessor.cs` ? `server_response` class
- [ ] Register `GetLicense` action in `ApiActionFactory.cs`

### Phase 4: Testing
- [ ] Test with V1 (legacy) license file
- [ ] Test with V2 (JSON) license file
- [ ] Test with invalid API credentials (should fail)
- [ ] Test with valid API credentials but expired license
- [ ] Test with evaluation license
- [ ] Test with community edition license

### Phase 5: Documentation
- [ ] Update API documentation
- [ ] Add to release notes

---

## Security Considerations

1. **Authentication Required**: The API validates API Key Id and Key pair before returning license data
2. **Auto-Granted Permission**: The `GetLicense` permission is automatically granted after API key validation - any valid API key can retrieve license information without needing explicit permission assignment
3. **Permission Design**: Adding `GetLicense` to `ApiPermissionsEnum` (ID 53) and handling it specially in `CheckAPIPermission` ensures:
   - Clean separation of concerns
   - Follows existing permission patterns
   - Does not require database schema changes
4. **Information Exposure**: License information is not considered sensitive but includes:
   - License limits (users, tokens)
   - Company information (if V2)
   - License key identifier (if V2)
5. **Audit Trail**: All requests are logged to the audit trail

---

## Dependencies

### Required References
- `SurePassClassLib.Utility.MfaServerLicense` - License reading
- `SurePassAdmin.WebUtil.GetLicenseFile()` - License file access
- `Newtonsoft.Json` - JSON serialization

### Related Classes
- `MfaServerLicense` - Main license class (in submodule)
- `LicenseUserData` - V2 license structure
- `Product`, `Company`, `Licensing` - V2 nested classes

---

## Summary of Changes

| File | Action | Description |
|------|--------|-------------|
| `server\Models\GetLicenseResponse.cs` | **CREATE** | Response model with nested classes |
| `server\ApiActions\GetLicense.cs` | **CREATE** | API action implementation |
| `SurePassClassLib\Enums\ApiPermissionsEnum.cs` | **MODIFY** | Add `GetLicense = 53` enum value |
| `SurePassClassLib\AuthServerAPI\AuthServerAPIAccessKeyCheck.cs` | **MODIFY** | Auto-grant GetLicense after API key validation |
| `server\RestApiProcessor.cs` | **MODIFY** | Add `GET_LICENSE` constant |
| `server\ApiActions\ApiActionFactory.cs` | **MODIFY** | Register new action |

---

## Questions for Review

1. Should the API require any specific permission, or is API key authentication sufficient?
2. Should any license fields be excluded from the response for security reasons?
3. Should the response include hostname validation status?
4. Should there be a "minimal" response option to reduce payload size?

---

*Document created: 2025*  
*For: SurePassID Authentication Services Release 2025.4*
