package main
import (
"context"
"fmt"
"io"
"os"
"slices"
"strconv"
"strings"
"time"
"encoding/json"
"net/http"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
argocd "github.com/argoproj-labs/argocd-ephemeral-access/api/argoproj/v1alpha1"
api "github.com/argoproj-labs/argocd-ephemeral-access/api/ephemeral-access/v1alpha1"
"github.com/hashicorp/go-hclog"
"github.com/argoproj-labs/argocd-ephemeral-access/pkg/log"
"github.com/argoproj-labs/argocd-ephemeral-access/pkg/plugin"
goPlugin "github.com/hashicorp/go-plugin"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type ServiceNowPlugin struct {
Logger hclog.Logger
}
type CmdbServiceNow struct {
InstallStatus string `json:"install_status"`
Name string `json:"name"`
SysId string `json:"sys_id"`
}
type CmdbResultsServiceNowType struct {
Result []*CmdbServiceNow `json:"result"`
}
type ChangeServiceNow struct {
Type string `json:"type"`
Number string `json:"number"`
EndDate string `json:"end_date"`
ShortDescription string `json:"short_description"`
StartDate string `json:"start_date"`
SysId string `json:"sys_id"`
}
type Change struct {
Type string
Number string
EndDate time.Time
ShortDescription string
StartDate time.Time
SysId string
}
type ChangeResultsServicenow struct {
Result []*ChangeServiceNow `json:"result"`
}
const SysparmLimit = 5
const ExclusionsConfigMapName = "controller-cm"
var unittest = false
var serviceNowUrl string
var serviceNowUsername string
var serviceNowPassword string
var ciLabel string
var exclusionRoles []string
var timezone string
var timeWindowChangesDays int
var k8sconfig *rest.Config
var k8sclientset kubernetes.Interface
var ephemeralAccessPluginNamespace string
func (p *ServiceNowPlugin) getEnvVarWithoutDefault(envVarName string, errorTextToReturn string) (string, string) {
errorText := ""
returnValue := os.Getenv(envVarName)
if returnValue == "" {
p.Logger.Error(errorTextToReturn)
errorText = errorTextToReturn
}
return returnValue, errorText
}
func (p *ServiceNowPlugin) getEnvVarWithDefault(envVarName string, envVarDefault string) string {
returnValue := os.Getenv(envVarName)
if returnValue == "" {
p.Logger.Debug(fmt.Sprintf("Environment variable %s is empty, assuming %s", envVarName, envVarDefault))
returnValue = envVarDefault
}
return returnValue
}
func (p *ServiceNowPlugin) getLocalTime(t time.Time) string {
loc, _ := time.LoadLocation(timezone)
return fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d",
t.In(loc).Year(),
t.In(loc).Month(),
t.In(loc).Day(),
t.In(loc).Hour(),
t.In(loc).Minute(),
t.In(loc).Second())
}
func (p *ServiceNowPlugin) convertTime(timestring string) (time.Time, string) {
goTimeString := strings.ReplaceAll(timestring, " ", "T") + "Z"
var goTime time.Time
err := goTime.UnmarshalText([]byte(goTimeString))
errorText := ""
if err != nil {
errorText = "Error in converting " + timestring + " to go Time: " + err.Error()
p.Logger.Error(errorText)
}
return goTime, errorText
}
func (p *ServiceNowPlugin) convertToInt(context string, s string, def int) int {
i, err := strconv.Atoi(s)
if err != nil {
errorText := fmt.Sprintf("Incorrect value, %s in %s: should be a number, assuming %d", s, context, def)
p.Logger.Error(errorText)
i = def
}
return i
}
func (p *ServiceNowPlugin) getK8sConfig() string {
var err error
errorText := ""
if !unittest {
k8sconfig, err = rest.InClusterConfig()
if err != nil {
errorText = "Error in getK8sConfig, rest.InClusterConfig: " + err.Error()
} else {
k8sclientset, err = kubernetes.NewForConfig(k8sconfig)
if err != nil {
errorText = "Error in getK8sConfig, kubernetes.NewForConfig: " + err.Error()
}
}
}
return errorText
}
func (p *ServiceNowPlugin) getCredentialsFromSecret(namespace string, secretName string, usernameKey string, passwordKey string) (string, string, string) {
p.Logger.Debug(fmt.Sprintf("Get credentials from secret [%s]%s...", namespace, secretName))
errorText := ""
secret, err := k8sclientset.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{})
if err != nil {
errorText = fmt.Sprintf("Error getting secret %s, does secret exist in namespace %s? Error: %s", secretName, namespace, err.Error())
p.Logger.Error(errorText)
}
return string(secret.Data[usernameKey]), string(secret.Data[passwordKey]), errorText
}
func (p *ServiceNowPlugin) getExclusionsFromConfigMap(namespace string) []string {
p.Logger.Debug(fmt.Sprintf("Get exclusions from configmap [%s]%s", namespace, ExclusionsConfigMapName))
exclusions := []string{}
configmap, err := k8sclientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), ExclusionsConfigMapName, metav1.GetOptions{})
if err != nil {
debugText := fmt.Sprintf("Error getting configmap %s, does configmap exist in namespace %s?", ExclusionsConfigMapName, namespace)
p.Logger.Debug(debugText)
p.Logger.Debug("No exclusions used")
} else {
exclusions = strings.Split(configmap.Data["exclusion-roles"], "\n")
p.Logger.Debug("Exclusions used: " + configmap.Data["exclusion-roles"])
}
return exclusions
}
func (p *ServiceNowPlugin) getGlobalVars() string {
errorText := p.getK8sConfig()
serviceNowURLError := ""
serviceNowCredentialsError := ""
serviceNowUrl, serviceNowURLError = p.getEnvVarWithoutDefault("SERVICENOW_URL", "No Service Now URL given (environment variable SERVICENOW_URL is empty)")
timezone = p.getEnvVarWithDefault("TIMEZONE", "UTC")
ciLabel = p.getEnvVarWithDefault("CI_LABEL", "ci-name")
ephemeralAccessPluginNamespace = p.getEnvVarWithDefault("EPHEMERAL_ACCESS_EXTENSION_NAMESPACE", "argocd-ephemeral-access")
exclusionRoles = p.getExclusionsFromConfigMap(ephemeralAccessPluginNamespace)
timeWindowChangesDays = p.convertToInt("environment variable TIME_WINDOW_CHANGES_DAYS", p.getEnvVarWithDefault("TIME_WINDOW_CHANGES_DAYS", "7"), 7)
serviceNowUsername, serviceNowPassword, serviceNowCredentialsError = p.getServiceNowCredentials()
return errorText + serviceNowURLError + serviceNowCredentialsError
}
func (p *ServiceNowPlugin) showRequest(ar *api.AccessRequest, app *argocd.Application) {
username := ar.Spec.Subject.Username
role := ar.Spec.Role.TemplateRef.Name
namespace := ar.Spec.Application.Namespace
applicationName := ar.Spec.Application.Name
duration := ar.Spec.Duration.Duration.String()
infoText := fmt.Sprintf("Call to GrantAccess: username: %s, role: %s, application: [%s]%s, duration: %s", username, role, namespace, applicationName, duration)
p.Logger.Info(infoText)
jsonAr, _ := json.Marshal(ar)
jsonApp, _ := json.Marshal(app)
p.Logger.Debug("jsonAr: " + string(jsonAr))
p.Logger.Debug("jsonApp: " + string(jsonApp))
}
func (p *ServiceNowPlugin) createRevokeJob(namespace string, accessrequestName string, jobStartTime time.Time) {
p.Logger.Debug(fmt.Sprintf("createRevokeJob: %s, %s", namespace, accessrequestName))
jobName := strings.ReplaceAll("stop-"+accessrequestName, ".", "-")
cmd := fmt.Sprintf("kubectl delete accessrequest -n argocd %s && kubectl delete cronjob -n argocd %s", accessrequestName, jobName)
cronjobs := k8sclientset.BatchV1().CronJobs(namespace)
var backOffLimit int32 = 0
cronJobSpec := &batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: jobName,
Namespace: namespace,
},
Spec: batchv1.CronJobSpec{
Schedule: fmt.Sprintf("%d %d %d %d *", jobStartTime.Minute(), jobStartTime.Hour(), jobStartTime.Day(), jobStartTime.Month()),
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
ServiceAccountName: "remove-accessrequest-job-sa",
Containers: []v1.Container{
{
Name: jobName,
Image: "bitnami/kubectl:latest",
Command: []string{"sh", "-c", cmd},
},
},
RestartPolicy: v1.RestartPolicyNever,
},
},
BackoffLimit: &backOffLimit,
},
},
},
}
_, err := cronjobs.Create(context.TODO(), cronJobSpec, metav1.CreateOptions{})
if err != nil {
p.Logger.Error(fmt.Sprintf("Failed to create K8s job %s in namespace %s: %s.", jobName, namespace, err.Error()))
} else {
p.Logger.Info(fmt.Sprintf("Created K8s job %s successfully in namespace %s", jobName, namespace))
}
}
// Set duration to the time left for this (valid) change, unless original request was
// shorter - then we are forced to use the duration of the original request.
// In an ideal world, the enddate should always be the enddate of the change and the duration always the amount of time
// that remains until that moment.
func (p *ServiceNowPlugin) determineDurationAndRealEndTime(arDuration time.Duration, changeRemainingTime time.Duration, changeEndDate time.Time) (time.Duration, time.Time) {
var duration time.Duration
var realEndTime time.Time
if arDuration > changeRemainingTime {
duration = changeRemainingTime
realEndTime = changeEndDate
} else {
duration = arDuration
realEndTime = time.Now().Add(arDuration)
}
return duration, realEndTime
}
func (p *ServiceNowPlugin) determineGrantedTextsChange(requesterName string, requestedRole string, validChange Change, remainingTime time.Duration, realEndDate time.Time) (string, string) {
grantedAccessText := fmt.Sprintf("Granted access for %s: %s change %s (%s), role %s, from %s to %s",
requesterName,
validChange.Type,
validChange.Number,
validChange.ShortDescription,
requestedRole,
time.Now().Truncate(time.Minute),
realEndDate.Truncate(time.Second).String())
grantedAccessUIText := fmt.Sprintf("Granted access: change __%s__ (%s), until __%s (%s)__",
validChange.Number,
validChange.ShortDescription,
p.getLocalTime(realEndDate),
remainingTime.Truncate(time.Second).String())
grantedAccessServiceNowText := fmt.Sprintf("ServiceNow plugin granted access to %s, for role %s, until %s (%s)",
requesterName,
requestedRole,
p.getLocalTime(realEndDate),
remainingTime.Truncate(time.Second).String())
p.Logger.Info(grantedAccessText)
p.Logger.Debug(grantedAccessUIText)
return grantedAccessUIText, grantedAccessServiceNowText
}
func (p *ServiceNowPlugin) determineGrantedTextsExclusions(requesterName string, requestedRole string, remainingTime time.Duration, realEndDate time.Time) string {
grantedAccessText := fmt.Sprintf("Granted access for %s: role %s, from %s to %s (no change, %s is an exclusion role)",
requesterName,
requestedRole,
time.Now().Truncate(time.Minute),
realEndDate.Truncate(time.Minute),
requestedRole)
grantedAccessUIText := fmt.Sprintf("Granted access: %s is an exclusion role, until __%s (%s)__",
requestedRole,
p.getLocalTime(realEndDate),
remainingTime.Truncate(time.Second).String())
p.Logger.Warn(grantedAccessText)
p.Logger.Debug(grantedAccessUIText)
return grantedAccessUIText
}
func (p *ServiceNowPlugin) denyRequest(reason string) (*plugin.GrantResponse, error) {
return &plugin.GrantResponse{
Status: plugin.GrantStatusDenied,
Message: reason,
}, nil
}
func (p *ServiceNowPlugin) grantRequest(reason string) (*plugin.GrantResponse, error) {
return &plugin.GrantResponse{
Status: plugin.GrantStatusGranted,
Message: reason,
}, nil
}
func (p *ServiceNowPlugin) getServiceNowCredentials() (string, string, string) {
secretName := p.getEnvVarWithDefault("SERVICENOW_SECRET_NAME", "servicenow-secret")
return p.getCredentialsFromSecret(ephemeralAccessPluginNamespace, secretName, "username", "password")
}
func (p *ServiceNowPlugin) checkAPIResult(resp *http.Response, body []byte) ([]byte, string) {
errorText := ""
if (resp.StatusCode >= 500 && resp.StatusCode <= 599) || strings.Contains(string(body), "<html>") {
errorText = "ServiceNow API server is down"
}
if resp.StatusCode >= 400 && resp.StatusCode <= 499 {
errorText = "ServiceNow API changed"
}
return body, errorText
}
func (p *ServiceNowPlugin) getFromServiceNowAPI(requestURI string) ([]byte, string) {
apiCall := fmt.Sprintf("%s%s", serviceNowUrl, requestURI)
p.Logger.Debug("apiCall: " + apiCall)
req, err := http.NewRequest("GET", apiCall, nil)
if err != nil {
errorText := "Error in NewRequest: " + err.Error()
p.Logger.Error(errorText)
return []byte{}, errorText
}
req.Header.Add("Accept", "application/json")
req.SetBasicAuth(serviceNowUsername, serviceNowPassword)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
errorText := "Error in client.Do: " + err.Error()
p.Logger.Error(errorText)
return []byte{}, errorText
}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
errorText := "Error in io.ReadAll: " + err.Error()
p.Logger.Error(errorText)
return []byte{}, errorText
}
p.Logger.Debug(string(body))
return p.checkAPIResult(resp, body)
}
func (p *ServiceNowPlugin) patchServiceNowAPI(requestURI string, data string) ([]byte, string) {
apiCall := fmt.Sprintf("%s%s", serviceNowUrl, requestURI)
p.Logger.Debug("apiCall: " + apiCall)
p.Logger.Debug("Data: " + string(data))
req, err := http.NewRequest("PATCH", apiCall, strings.NewReader(data))
if err != nil {
errorText := "Error in NewRequest: " + err.Error()
p.Logger.Error(errorText)
return nil, errorText
}
req.Header.Add("Accept", "application/json")
req.SetBasicAuth(serviceNowUsername, serviceNowPassword)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
errorText := "Error in client.Do: " + err.Error()
p.Logger.Error(errorText)
return nil, errorText
}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
errorText := "Error in io.ReadAll: " + err.Error()
p.Logger.Error(errorText)
return nil, errorText
}
p.Logger.Debug("Body: " + string(body))
return p.checkAPIResult(resp, body)
}
func (p *ServiceNowPlugin) getCIName(app *argocd.Application) string {
p.Logger.Debug("Search for " + ciLabel + " in the CMDB...")
ciName := string(app.Labels[ciLabel])
p.Logger.Debug(fmt.Sprintf("ciLabel %s found: %s", ciLabel, ciName))
return ciName
}
func (p *ServiceNowPlugin) getCI(ciName string) (*CmdbServiceNow, string) {
requestURI := fmt.Sprintf("/api/now/table/cmdb_ci?name=%s&sysparm_fields=install_status,name,sys_id", ciName)
response, errorText := p.getFromServiceNowAPI(requestURI)
if errorText != "" {
return nil, errorText
}
var cmdbResults CmdbResultsServiceNowType
err := json.Unmarshal(response, &cmdbResults)
if err != nil {
errorText := fmt.Sprintf("Error in json.Unmarshal: %s (%s)", err.Error(), response)
p.Logger.Error(errorText)
return nil, errorText
}
if len(cmdbResults.Result) == 0 {
errorText := fmt.Sprintf("No CI with name %s found", ciName)
p.Logger.Error(errorText)
return nil, errorText
}
debugText := fmt.Sprintf("InstallStatus: %s, CI name: %s, SysId: %s",
cmdbResults.Result[0].InstallStatus,
cmdbResults.Result[0].Name,
cmdbResults.Result[0].SysId)
p.Logger.Debug(debugText)
return cmdbResults.Result[0], ""
}
func (p *ServiceNowPlugin) getChangeRequestURI(ciSysId string, sysparmOffset int) string {
// See also: https://github.com/argoproj-labs/argocd-ephemeral-access/issues/109
// Hopefully the Ephemeral Access Extension will be extended with a reference number
// that can be used to ask directly for the change number.
//
// In that case the original request will be extended with the change number and the
// request will be better readable as well (the dates can only be used with > and <
// in the sysparam_query, that also needs to have the cmdb_ci sys_id instead of the
// name and -1 instead of the display name of the state)
//
// Original requestURI without test for change number:
// requestURI := fmt.Sprintf("/api/now/table/change_request?cmdb_ci=%s&state=Implement&phase=Requested&approval=Approved&active=true&sysparm_fields=type,number,short_description,start_date,end_date,sys_id&sysparm_limit=%d&sysparm_offset=%d", ciName, SysparmLimit, SysparmOffset)
//
// The reason for the window is to limit the number of changes that have to be
// processed by the API in large environments.
window, _ := time.ParseDuration(fmt.Sprintf("%d", timeWindowChangesDays*24) + "h")
fromDate := time.Now().Add(-window)
endDate := time.Now().Add(window)
fromDateString := fmt.Sprintf(`%04d-%02d-%02d 00:00:00`,
fromDate.Year(),
fromDate.Month(),
fromDate.Day())
endDateString := fmt.Sprintf(`%04d-%02d-%02d 23:59:59`,
endDate.Year(),
endDate.Month(),
endDate.Day())
selection := fmt.Sprintf("cmdb_ci=%s&state=-1&phase=requested&approval=approved&active=true&GOTOstart_date>%s&GOTOend_date<%s",
ciSysId,
fromDateString,
endDateString)
// selection should be encoded for url (the rest doesn't matter)
selection = strings.ReplaceAll(selection, " ", "%20")
selection = strings.ReplaceAll(selection, "-", "%2d")
selection = strings.ReplaceAll(selection, ":", "%3a")
selection = strings.ReplaceAll(selection, "<", "%3c")
selection = strings.ReplaceAll(selection, "=", "%3d")
selection = strings.ReplaceAll(selection, ">", "%3e")
// sysparam_query uses ^ to combine fields instead of &
selection = strings.ReplaceAll(selection, "&", "%5e")
otherFields := fmt.Sprintf("sysparm_fields=type,number,short_description,start_date,end_date,sys_id&sysparm_limit=%d&sysparm_offset=%d",
SysparmLimit,
sysparmOffset)
requestURI := "/api/now/table/change_request?sysparm_query=" + selection + "&" + otherFields
return requestURI
}
func (p *ServiceNowPlugin) getChanges(ciSysId string, sysparmOffset int) ([]*ChangeServiceNow, int, string) {
requestURI := p.getChangeRequestURI(ciSysId, sysparmOffset)
response, errorText := p.getFromServiceNowAPI(requestURI)
if errorText != "" {
p.Logger.Error(errorText)
return nil, sysparmOffset, errorText
}
var changeResults ChangeResultsServicenow
err := json.Unmarshal(response, &changeResults)
if err != nil {
errorText := fmt.Sprintf("Error in json.Unmarshal: %s (%s)", err.Error(), response)
p.Logger.Error(errorText)
return nil, sysparmOffset, errorText
}
if len(changeResults.Result) == 0 {
errorText = "No changes found"
p.Logger.Info(errorText)
}
return changeResults.Result, sysparmOffset + len(changeResults.Result), errorText
}
func (p *ServiceNowPlugin) parseChange(changeServiceNow ChangeServiceNow) (Change, string) {
var change Change
p.Logger.Debug(fmt.Sprintf("Change: Type: %s, Number: %s, Short description: %s, Start Date: %s, End Date: %s, SysId: %s",
changeServiceNow.Type,
changeServiceNow.Number,
changeServiceNow.ShortDescription,
changeServiceNow.StartDate,
changeServiceNow.EndDate,
changeServiceNow.SysId))
errorTextStartDate := ""
errorTextEndDate := ""
change.Type = changeServiceNow.Type
change.Number = changeServiceNow.Number
change.ShortDescription = changeServiceNow.ShortDescription
change.StartDate, errorTextStartDate = p.convertTime(changeServiceNow.StartDate)
change.EndDate, errorTextEndDate = p.convertTime(changeServiceNow.EndDate)
change.SysId = changeServiceNow.SysId
return change, errorTextStartDate + errorTextEndDate
}
func (p *ServiceNowPlugin) checkCI(CI CmdbServiceNow) string {
errorText := ""
installStatus := CI.InstallStatus
ciName := CI.Name
validInstallStatus := []string{
"1", // Installed
"3", // In maintenance
"4", // Pending install
"5", // Pending repair
}
if !slices.Contains(validInstallStatus, installStatus) {
errorText = fmt.Sprintf("Invalid install status (%s) for CI %s", installStatus, ciName)
}
return errorText
}
func (p *ServiceNowPlugin) checkChange(change Change) (string, time.Duration) {
errorText := ""
var remainingTime time.Duration
remainingTime = 0
currentTime := time.Now()
if change.EndDate.Before(currentTime) ||
change.StartDate.After(currentTime) {
errorText = fmt.Sprintf("Change %s (%s) is not in the valid time range. start date: %s and end date: %s (current date: %s)",
change.Number,
change.ShortDescription,
p.getLocalTime(change.StartDate),
p.getLocalTime(change.EndDate),
p.getLocalTime(currentTime))
p.Logger.Debug(errorText)
} else {
remainingTime = time.Until(change.EndDate)
}
return errorText, remainingTime
}
func (p *ServiceNowPlugin) processCI(ciName string) (string, string) {
CI, errorText := p.getCI(ciName)
if errorText != "" {
p.Logger.Error(errorText)
return errorText, ""
}
errorText = p.checkCI(*CI)
return errorText, CI.SysId
}
func (p *ServiceNowPlugin) processChanges(ciName string, ciSysId string) (string, time.Duration, *Change) {
var SysparmOffset = 0
serviceNowChanges, SysparmOffset, errorText := p.getChanges(ciSysId, SysparmOffset)
if errorText != "" {
var noDuration = 0 * time.Minute
return errorText, noDuration, nil
}
var validChange *Change
var changeRemainingTime time.Duration
var remainingTime time.Duration
for {
for _, serviceNowChange := range serviceNowChanges {
change, errorText := p.parseChange(*serviceNowChange)
if errorText == "" {
errorText, remainingTime = p.checkChange(change)
if errorText == "" {
validChange = &change
changeRemainingTime = remainingTime
break
}
}
}
if validChange != nil {
break
} else if len(serviceNowChanges) < SysparmLimit {
errorText = "No valid change found"
break
} else {
serviceNowChanges, SysparmOffset, errorText = p.getChanges(ciSysId, SysparmOffset)
if errorText != "" {
break
}
}
}
return errorText, changeRemainingTime, validChange
}
func (p *ServiceNowPlugin) postNote(sysId string, noteText string) {
requestURI := fmt.Sprintf("/api/now/table/change_request/%s", sysId)
p.patchServiceNowAPI(requestURI, noteText)
}
// Public methods
func (p *ServiceNowPlugin) Init() error {
p.Logger.Debug("This is a call to the Init method")
// p.getGlobalVars cannot be put in the Init method: the variables will be lost between different calls
return nil
}
func (p *ServiceNowPlugin) GrantAccess(ar *api.AccessRequest, app *argocd.Application) (*plugin.GrantResponse, error) {
p.Logger.Debug("This is a call to the GrantAccess method")
p.showRequest(ar, app)
requesterName := ar.Spec.Subject.Username
requestedRole := ar.Spec.Role.TemplateRef.Name
namespace := ar.Spec.Application.Namespace
arName := ar.Name
arDuration := ar.Spec.Duration.Duration
applicationName := ar.Spec.Application.Name
errorText := p.getGlobalVars()
if errorText != "" {
p.Logger.Error(errorText)
return p.denyRequest(errorText)
}
if slices.Contains(exclusionRoles, requestedRole) {
endTime := time.Now().Add(arDuration)
grantedUIText := p.determineGrantedTextsExclusions(requesterName, requestedRole, arDuration, endTime)
return p.grantRequest(grantedUIText)
}
ciName := p.getCIName(app)
if ciName == "\"\"" {
errorText := fmt.Sprintf("No CI name found: expected label with name %s in application %s", ciLabel, applicationName)
p.Logger.Error(errorText)
return p.denyRequest(errorText)
}
errorString, ciSysId := p.processCI(ciName)
if errorString != "" {
p.Logger.Error("Access Denied for " + requesterName + " : " + errorString)
return p.denyRequest(errorString)
}
errorString, changeRemainingTime, validChange := p.processChanges(ciName, ciSysId)
if errorString == "" {
duration, endDateTime := p.determineDurationAndRealEndTime(arDuration, changeRemainingTime, validChange.EndDate)
ar.Spec.Duration.Duration = duration
// AbortJob is only needed when the end date of the change is earlier than the default for the access request time in
// the future, otherwise the ArgoCD Ephemeral Access Extension will revoke the permissions
if arDuration > changeRemainingTime {
p.createRevokeJob(namespace, arName, validChange.EndDate)
}
jsonAr, _ := json.Marshal(ar)
p.Logger.Debug(string(jsonAr))
grantedUIText, grantedAccessServiceNowText := p.determineGrantedTextsChange(requesterName, requestedRole, *validChange, duration, endDateTime)
note := fmt.Sprintf("{\"work_notes\":\"%s\"}", grantedAccessServiceNowText)
p.postNote(validChange.SysId, note)
return p.grantRequest(grantedUIText)
} else {
p.Logger.Warn(fmt.Sprintf("Access Denied for %s, role %s: %s", requesterName, requestedRole, errorString))
return p.denyRequest(errorString)
}
}
func (p *ServiceNowPlugin) RevokeAccess(ar *api.AccessRequest, app *argocd.Application) (*plugin.RevokeResponse, error) {
return nil, nil
}
func main() {
logger, err := log.NewPluginLogger()
if err != nil {
panic(fmt.Sprintf("Error creating plugin logger: %s", err))
}
p := &ServiceNowPlugin{
Logger: logger,
}
srvConfig := plugin.NewServerConfig(p, logger)
goPlugin.Serve(srvConfig)
}