-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[CLOUDTRUST-1157] Add user_id in events + allow generic HTTP reponse #15
Changes from all commits
da67367
22988a4
b947109
7fd9f00
b7ec231
73ec38f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package database | |
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"time" | ||
|
||
cs "github.com/cloudtrust/common-service" | ||
|
@@ -27,6 +28,27 @@ const ( | |
` | ||
) | ||
|
||
// Defines event information constants | ||
const ( | ||
CtEventType = "ct_event_type" | ||
CtEventAgentUsername = "agent_username" | ||
CtEventAgentRealmName = "agent_realm_name" | ||
CtEventUserID = "user_id" | ||
CtEventOrigin = "origin" | ||
CtEventAuditTime = "audit_time" | ||
CtEventRealmName = "realm_name" | ||
CtEventAgentUserID = "agent_user_id" | ||
CtEventUsername = "username" | ||
CtEventKcEventType = "kc_event_type" | ||
CtEventKcOperationType = "kc_operation_type" | ||
CtEventClientID = "client_id" | ||
CtEventAdditionalInfo = "additional_info" | ||
) | ||
|
||
var ctEventColumns = []string{ | ||
CtEventType, CtEventAgentUsername, CtEventAgentRealmName, CtEventUserID, CtEventOrigin, CtEventAuditTime, CtEventRealmName, | ||
CtEventAgentUserID, CtEventUsername, CtEventKcEventType, CtEventKcOperationType, CtEventClientID, CtEventAdditionalInfo} | ||
|
||
// EventsDBModule is the interface of the audit events module. | ||
type EventsDBModule interface { | ||
Store(context.Context, map[string]string) error | ||
|
@@ -37,6 +59,32 @@ type eventsDBModule struct { | |
db CloudtrustDB | ||
} | ||
|
||
func isInArray(array []string, value string) bool { | ||
for _, e := range array { | ||
if e == value { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func checkNull(value string) interface{} { | ||
if value == "" { | ||
return nil | ||
} | ||
return value | ||
} | ||
|
||
// CreateAdditionalInfo creates the additional info value | ||
func CreateAdditionalInfo(values ...string) string { | ||
var nfo = make(map[string]string) | ||
for i := 0; i+1 < len(values); i += 2 { | ||
nfo[values[i]] = values[i+1] | ||
} | ||
addInfo, _ := json.Marshal(nfo) | ||
return string(addInfo) | ||
} | ||
|
||
// NewEventsDBModule returns a Console module. | ||
func NewEventsDBModule(db CloudtrustDB) EventsDBModule { | ||
//db.Exec(createTable) | ||
|
@@ -48,41 +96,54 @@ func NewEventsDBModule(db CloudtrustDB) EventsDBModule { | |
func (cm *eventsDBModule) Store(_ context.Context, m map[string]string) error { | ||
// if ctEventType is not "", then record the events in MariaDB | ||
// otherwise, do nothing | ||
if m["ct_event_type"] == "" { | ||
if m[CtEventType] == "" { | ||
return nil | ||
} | ||
|
||
// the event was already formatted according to the DB structure already at the component level | ||
|
||
//auditTime - time of the event | ||
auditTime := m["audit_time"] | ||
auditTime := m[CtEventAuditTime] | ||
// origin - the component that initiated the event | ||
origin := m["origin"] | ||
origin := m[CtEventOrigin] | ||
// realmName - realm name of the user that is impacted by the action | ||
realmName := m["realm_name"] | ||
realmName := m[CtEventRealmName] | ||
//agentUserID - userId of who is performing an action | ||
agentUserID := m["agent_user_id"] | ||
agentUserID := m[CtEventAgentUserID] | ||
//agentUsername - username of who is performing an action | ||
agentUsername := m["agent_username"] | ||
agentUsername := m[CtEventAgentUsername] | ||
//agentRealmName - realm of who is performing an action | ||
agentRealmName := m["agent_realm_name"] | ||
agentRealmName := m[CtEventAgentRealmName] | ||
//userID - ID of the user that is impacted by the action | ||
userID := m["user_id"] | ||
userID := m[CtEventUserID] | ||
//username - username of the user that is impacted by the action | ||
username := m["username"] | ||
username := m[CtEventUsername] | ||
// ctEventType that is established before at the component level | ||
ctEventType := m["ct_event_type"] | ||
ctEventType := m[CtEventType] | ||
// kcEventType corresponds to keycloak event type | ||
kcEventType := m["kc_event_type"] | ||
kcEventType := m[CtEventKcEventType] | ||
// kcOperationType - operation type of the event that comes from Keycloak | ||
kcOperationType := m["kc_operation_type"] | ||
kcOperationType := m[CtEventKcOperationType] | ||
// Id of the client | ||
clientID := m["client_id"] | ||
clientID := m[CtEventClientID] | ||
//additional_info - all the rest of the information from the event | ||
additionalInfo := m["additional_info"] | ||
additionalInfo := m[CtEventAdditionalInfo] | ||
if additionalInfo == "" { | ||
var addNfo = make(map[string]string) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nfo ? is it a typo for Info ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I often write Nfo for information... or Info... yes, I'm a little bit lazy |
||
for k, v := range m { | ||
if !isInArray(ctEventColumns, k) { | ||
addNfo[k] = v | ||
} | ||
} | ||
if additionalInfoBytes, err := json.Marshal(addNfo); err == nil && len(addNfo) > 0 { | ||
additionalInfo = string(additionalInfoBytes) | ||
} | ||
} | ||
|
||
//store the event in the DB | ||
_, err := cm.db.Exec(insertEvent, auditTime, origin, realmName, agentUserID, agentUsername, agentRealmName, userID, username, ctEventType, kcEventType, kcOperationType, clientID, additionalInfo) | ||
_, err := cm.db.Exec(insertEvent, auditTime, origin, checkNull(realmName), checkNull(agentUserID), checkNull(agentUsername), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to check for empty string ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better to have NULL in database rather than empty string. NULL is easier to manage when searching for events |
||
checkNull(agentRealmName), checkNull(userID), checkNull(username), checkNull(ctEventType), checkNull(kcEventType), | ||
checkNull(kcOperationType), checkNull(clientID), checkNull(additionalInfo)) | ||
|
||
return err | ||
} | ||
|
@@ -109,9 +170,9 @@ type ReportEventDetails struct { | |
func CreateEvent(apiCall string, origin string) ReportEventDetails { | ||
var event ReportEventDetails | ||
event.details = make(map[string]string) | ||
event.details["ct_event_type"] = apiCall | ||
event.details["origin"] = origin | ||
event.details["audit_time"] = time.Now().UTC().Format(timeFormat) | ||
event.details[CtEventType] = apiCall | ||
event.details[CtEventOrigin] = origin | ||
event.details[CtEventAuditTime] = time.Now().UTC().Format(timeFormat) | ||
|
||
return event | ||
} | ||
|
@@ -127,11 +188,15 @@ func (er *ReportEventDetails) AddEventValues(values ...string) { | |
|
||
// AddAgentDetails add details from the context | ||
func (er *ReportEventDetails) AddAgentDetails(ctx context.Context) { | ||
//retrieve agent username | ||
er.details["agent_username"] = ctx.Value(cs.CtContextUsername).(string) | ||
//retrieve agent user id - not yet implemented | ||
//to be uncommented once the ctx contains the userId value | ||
//er.details["userId"] = ctx.Value(cs.CtContextUserID).(string) | ||
//retrieve agent realm | ||
er.details["agent_realm_name"] = ctx.Value(cs.CtContextRealm).(string) | ||
var mapper = map[cs.CtContext]string{ | ||
cs.CtContextUsername: CtEventAgentUsername, | ||
cs.CtContextUserID: CtEventUserID, | ||
cs.CtContextRealm: CtEventAgentRealmName, | ||
} | ||
for keyFrom, keyTo := range mapper { | ||
var value = ctx.Value(keyFrom) | ||
if value != nil { | ||
er.details[keyTo] = value.(string) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,59 @@ import ( | |
"github.com/pkg/errors" | ||
) | ||
|
||
// MimeContent defines a mime content for HTTP responses | ||
type MimeContent struct { | ||
Filename string | ||
MimeType string | ||
Content []byte | ||
} | ||
|
||
// GenericResponse for HTTP requests | ||
type GenericResponse struct { | ||
StatusCode int | ||
Headers map[string]string | ||
MimeContent *MimeContent | ||
JSONableResponse interface{} | ||
} | ||
|
||
// WriteResponse writes a response for a mime content type | ||
func (r *GenericResponse) WriteResponse(w http.ResponseWriter) { | ||
if r.Headers == nil { | ||
r.Headers = make(map[string]string, 0) | ||
} | ||
// Headers | ||
if r.MimeContent != nil { | ||
r.Headers["Content-Type"] = r.MimeContent.MimeType | ||
if len(r.MimeContent.Filename) > 0 { | ||
// Does not support UTF-8 or spaces in filename | ||
r.Headers["Content-Disposition"] = "attachment; filename=" + r.MimeContent.Filename | ||
} | ||
} | ||
for k, v := range r.Headers { | ||
w.Header().Set(k, v) | ||
} | ||
|
||
// Status code | ||
w.WriteHeader(r.StatusCode) | ||
|
||
// Body | ||
if r.MimeContent != nil { | ||
w.Write(r.MimeContent.Content) | ||
} else if r.JSONableResponse != nil { | ||
writeJSON(r.JSONableResponse, w) | ||
} | ||
} | ||
|
||
func writeJSON(jsonableResponse interface{}, w http.ResponseWriter) { | ||
w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||
|
||
var json, err = json.MarshalIndent(jsonableResponse, "", " ") | ||
|
||
if err == nil { | ||
w.Write(json) | ||
} | ||
} | ||
|
||
// BasicDecodeRequest does not expect parameters | ||
func BasicDecodeRequest(ctx context.Context, req *http.Request) (interface{}, error) { | ||
return DecodeRequest(ctx, req, map[string]string{}, map[string]string{}) | ||
|
@@ -78,13 +131,12 @@ func EncodeReply(_ context.Context, w http.ResponseWriter, rep interface{}) erro | |
return nil | ||
} | ||
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8") | ||
w.WriteHeader(http.StatusOK) | ||
|
||
var json, err = json.MarshalIndent(rep, "", " ") | ||
|
||
if err == nil { | ||
w.Write(json) | ||
switch e := rep.(type) { | ||
case GenericResponse: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GenericResponse is HTTP oriented. |
||
e.WriteResponse(w) | ||
default: | ||
w.WriteHeader(http.StatusOK) | ||
writeJSON(rep, w) | ||
} | ||
|
||
return nil | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do it only if additionalInfo is empty but I think we should do it anyway.
Initially Sonia did it somewhere else, is it moved here ? or done twice ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done by Sonia in the bridge... This way, it remains compatible with her version and new developments but you have to choose to set additional_info by your self OR let this function set it