230 lines
6.8 KiB
Go
230 lines
6.8 KiB
Go
package audit
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-resty/resty/v2"
|
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
|
)
|
|
|
|
type ClientOpts struct {
|
|
Enabled bool
|
|
URL string
|
|
ProductKey string
|
|
ProductName string
|
|
SharedKey string
|
|
SecretKey string
|
|
}
|
|
|
|
// Client interface for communicating with audit system
|
|
type Client struct {
|
|
enabled bool
|
|
apiSignature APISignature
|
|
opts *ClientOpts
|
|
client *resty.Client
|
|
}
|
|
|
|
func NewAuditClient(opts *ClientOpts) (*Client, error) {
|
|
if opts.Enabled {
|
|
log.Print("Audit entries will be created since OAUTH2_PROXY_ENABLE_AUDIT is true")
|
|
err := opts.validate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
log.Print("Audit entries will NOT be created since OAUTH2_PROXY_ENABLE_AUDIT is false")
|
|
}
|
|
apiSignature := NewAPISignature(opts.SecretKey, opts.SharedKey)
|
|
client := resty.New()
|
|
client.SetRetryCount(3).
|
|
SetRetryWaitTime(5 * time.Second).
|
|
SetRetryMaxWaitTime(20 * time.Second).
|
|
SetContentLength(true).
|
|
SetRetryAfter(func(_ *resty.Client, _ *resty.Response) (time.Duration, error) {
|
|
return 0, fmt.Errorf("%w: retry quota exceeded", ErrPersitAuditEvent)
|
|
})
|
|
return &Client{enabled: opts.Enabled, apiSignature: apiSignature, client: client, opts: opts}, nil
|
|
}
|
|
|
|
func (c *Client) CreateSuccessfulLoginAuditEntry(ss *sessions.SessionState, appURL string, tenantID string, req *http.Request) {
|
|
coding := Coding{
|
|
System: "http://hl7.org/fhir/ValueSet/audit-event-type", Version: "1", Code: "110114", Display: "User Authentication"}
|
|
c.createAuditEntry(ss, appURL, tenantID, "0", "Success", &coding, req)
|
|
}
|
|
|
|
func (c *Client) CreateFailedLoginAuditEntry(ss *sessions.SessionState, appURL string, tenantID string, errorDesc string, req *http.Request) {
|
|
coding := Coding{
|
|
System: "http://hl7.org/fhir/ValueSet/audit-event-type", Version: "1", Code: "110114", Display: "User Authentication"}
|
|
c.createAuditEntry(ss, appURL, tenantID, "1", errorDesc, &coding, req)
|
|
}
|
|
|
|
func (c *Client) CreateSuccessfulLogoutAuditEntry(ss *sessions.SessionState, appURL string, tenantID string, req *http.Request) {
|
|
coding := Coding{
|
|
System: "http://hl7.org/fhir/ValueSet/audit-event-type", Version: "1", Code: "110123", Display: "User Logout All Sessions"}
|
|
c.createAuditEntry(ss, appURL, tenantID, "0", "Success", &coding, req)
|
|
}
|
|
|
|
func (c *Client) CreateSuccessfulRevokeAccessTokenAuditEntry(ss *sessions.SessionState, appURL string, tenantID string, req *http.Request) {
|
|
coding := Coding{
|
|
System: "http://hl7.org/fhir/ValueSet/audit-event-type", Version: "1", Code: "110123", Display: "User revoked access token"}
|
|
c.createAuditEntry(ss, appURL, tenantID, "0", "Success", &coding, req)
|
|
}
|
|
|
|
func (c *Client) createAuditEntry(ss *sessions.SessionState, appURL string, tenantID string, outcomeCode string, outcomeDesc string, coding *Coding, req *http.Request) {
|
|
if !c.enabled {
|
|
return
|
|
}
|
|
|
|
// Service/component identifier
|
|
serviceIDs := []string{"oauth2proxy"}
|
|
var targetIP string
|
|
if hn, err := os.Hostname(); err == nil {
|
|
serviceIDs = append(serviceIDs, hn)
|
|
addrs, err := net.LookupHost(hn)
|
|
if err == nil && len(addrs) > 0 {
|
|
targetIP = addrs[0]
|
|
} else {
|
|
targetIP = hn
|
|
}
|
|
} else {
|
|
targetIP = "unknown"
|
|
}
|
|
|
|
// Extract source IP address
|
|
sourceIP := ""
|
|
if req != nil {
|
|
sourceIP = req.RemoteAddr
|
|
if xff := req.Header.Get("X-Forwarded-For"); xff != "" {
|
|
ips := strings.Split(xff, ",")
|
|
sourceIP = strings.TrimSpace(ips[0])
|
|
}
|
|
}
|
|
|
|
auditObject := RootEvent{
|
|
ResourceType: "AuditEvent",
|
|
Event: &Event{
|
|
Type: &Coding{System: coding.System, Version: coding.Version, Code: coding.Code, Display: coding.Display},
|
|
Action: "E",
|
|
DateTime: time.Now().UTC().Format(time.RFC3339),
|
|
Outcome: outcomeCode,
|
|
OutcomeDesc: outcomeDesc},
|
|
Participant: []*Participant{
|
|
{AltID: ss.User, UserID: UserID{Value: ss.Email}, Name: ss.PreferredUsername, Requestor: true}},
|
|
Source: Source{
|
|
Identifier: Identifier{
|
|
Type: &Coding{
|
|
System: "http://hl7.org/fhir/ValueSet/audit-source-type",
|
|
Code: "4",
|
|
Display: "Application Server",
|
|
},
|
|
Value: ss.Email,
|
|
},
|
|
Type: []*Coding{{System: "http://hl7.org/fhir/security-source-type", Code: "1", Display: "End-user display device, diagnostic device."}},
|
|
Extension: []*Extension{
|
|
{
|
|
URL: appURL,
|
|
Extension: []*ExtensionContent{
|
|
{
|
|
URL: "applicationName",
|
|
ValueString: c.opts.ProductName,
|
|
},
|
|
{
|
|
URL: "applicationVersion",
|
|
ValueString: "1",
|
|
},
|
|
{
|
|
URL: "serverName",
|
|
ValueString: "oauth2proxy",
|
|
},
|
|
{
|
|
URL: "componentName",
|
|
ValueString: "oauth2proxy",
|
|
},
|
|
{
|
|
URL: "productKey",
|
|
ValueString: c.opts.ProductKey,
|
|
},
|
|
{
|
|
URL: "tenant",
|
|
ValueString: tenantID,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ServiceIDs: serviceIDs,
|
|
SourceIPAddress: sourceIP,
|
|
TargetIPAddress: targetIP,
|
|
}
|
|
|
|
auditMessage, err := json.Marshal(auditObject)
|
|
if err != nil {
|
|
logger.Errorf("%s: could not marshal the audit object: %v", ErrPersitAuditEvent.Error(), err)
|
|
AuditErrorMetricCounter.Inc()
|
|
return
|
|
}
|
|
err = c.send(string(auditMessage))
|
|
if err != nil {
|
|
logger.Errorf("%s: could not send the audit message to the url '%s': %v", ErrPersitAuditEvent.Error(), c.opts.URL, err)
|
|
AuditErrorMetricCounter.Inc()
|
|
return
|
|
}
|
|
}
|
|
|
|
func (c *Client) send(msg string) error {
|
|
signedDate := time.Now().UTC().Format(time.RFC3339)
|
|
signature := c.apiSignature.GetSignature(signedDate)
|
|
resp, err := c.client.R().
|
|
SetHeader("Content-Type", "application/json").
|
|
SetHeader("api-version", "2").
|
|
SetHeader("HSDP-API-Signature", signature).
|
|
SetHeader("SignedDate", signedDate).
|
|
SetBody(msg).
|
|
Post(c.opts.URL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode() != 201 {
|
|
log.Println("Not able to send the audit message ", resp)
|
|
return fmt.Errorf("not able to persist audit, audit server returned %v", resp.StatusCode())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *ClientOpts) validate() error {
|
|
err := errors.New("")
|
|
if strings.TrimSpace(c.URL) == "" {
|
|
err = errors.New("the OAUTH2_PROXY_AUDIT_URL must be set")
|
|
}
|
|
|
|
if strings.TrimSpace(c.ProductName) == "" {
|
|
err = fmt.Errorf("%w: the OAUTH2_PROXY_AUDIT_PRODUCT_NAME must be set", err)
|
|
}
|
|
|
|
if strings.TrimSpace(c.ProductKey) == "" {
|
|
err = fmt.Errorf("%w: the OAUTH2_PROXY_AUDIT_PRODUCT_KEY must be set", err)
|
|
}
|
|
|
|
if strings.TrimSpace(c.SharedKey) == "" {
|
|
err = fmt.Errorf("%w: the OAUTH2_PROXY_AUDIT_SHARED_KEY must be set", err)
|
|
}
|
|
|
|
if strings.TrimSpace(c.SecretKey) == "" {
|
|
err = fmt.Errorf("%w: the OAUTH2_PROXY_AUDIT_SECRET_KEY must be set", err)
|
|
}
|
|
|
|
if err != nil && err.Error() != "" {
|
|
return fmt.Errorf("the OAUTH2_PROXY_ENABLE_AUDIT is set to true however these are missing: %w", err)
|
|
}
|
|
return nil
|
|
}
|