Skip to content

Commit

Permalink
[CLOUDTRUST-1157] Add user_id in events+allow generic HTTP reponse
Browse files Browse the repository at this point in the history
  • Loading branch information
fperot74 committed Jun 5, 2019
1 parent 57a00d3 commit cdc73eb
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 32 deletions.
115 changes: 90 additions & 25 deletions database/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package database

import (
"context"
"encoding/json"
"time"

cs "github.com/cloudtrust/common-service"
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
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),
checkNull(agentRealmName), checkNull(userID), checkNull(username), checkNull(ctEventType), checkNull(kcEventType),
checkNull(kcOperationType), checkNull(clientID), checkNull(additionalInfo))

return err
}
Expand All @@ -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
}
Expand All @@ -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)
}
}
}
8 changes: 8 additions & 0 deletions database/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package database

import (
"context"
"strings"
"testing"

cs "github.com/cloudtrust/common-service"
Expand All @@ -10,6 +11,13 @@ import (
"github.com/stretchr/testify/assert"
)

func TestCreateAdditionalInfo(t *testing.T) {
var addInfo = CreateAdditionalInfo("a", "b", "c", "d", "z")
assert.True(t, strings.Contains(addInfo, `"a":"b"`))
assert.True(t, strings.Contains(addInfo, `"c":"d"`))
assert.False(t, strings.Contains(addInfo, `"z"`))
}

func TestEventsDBModule(t *testing.T) {
var mockCtrl = gomock.NewController(t)
defer mockCtrl.Finish()
Expand Down
66 changes: 59 additions & 7 deletions http/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down Expand Up @@ -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:
e.WriteResponse(w)
default:
w.WriteHeader(http.StatusOK)
writeJSON(rep, w)
}

return nil
Expand Down
61 changes: 61 additions & 0 deletions http/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,67 @@ import (
"github.com/stretchr/testify/assert"
)

func makeHandler(e endpoint.Endpoint) *http_transport.Server {
return http_transport.NewServer(e,
func(ctx context.Context, req *http.Request) (interface{}, error) {
return BasicDecodeRequest(ctx, req)
},
EncodeReply,
http_transport.ServerErrorEncoder(ErrorHandlerNoLog()),
)
}

func TestHttpGenericResponse(t *testing.T) {
r := mux.NewRouter()
// Test with JSON content
r.Handle("/path/to/mime", makeHandler(func(_ context.Context, _ interface{}) (response interface{}, err error) {
return GenericResponse{
StatusCode: http.StatusNotFound,
Headers: map[string]string{"Location": "here"},
MimeContent: nil,
JSONableResponse: make([]int, 0),
}, nil
}))
// Test with MimeContent
r.Handle("/path/to/jpeg", makeHandler(func(_ context.Context, _ interface{}) (response interface{}, err error) {
mime := MimeContent{
MimeType: "image/jpg",
Content: []byte("not a real jpeg"),
Filename: "filename.jpg",
}
return GenericResponse{
StatusCode: http.StatusCreated,
Headers: nil,
MimeContent: &mime,
JSONableResponse: nil,
}, nil
}))

ts := httptest.NewServer(r)
defer ts.Close()

{
res, err := http.Get(ts.URL + "/path/to/mime")
assert.Nil(t, err)
assert.Equal(t, http.StatusNotFound, res.StatusCode)
assert.Equal(t, "here", res.Header.Get("Location"))

buf := new(bytes.Buffer)
buf.ReadFrom(res.Body)
assert.Equal(t, "[]", buf.String())
}

{
res, err := http.Get(ts.URL + "/path/to/jpeg")
assert.Nil(t, err)
assert.Equal(t, http.StatusCreated, res.StatusCode)

buf := new(bytes.Buffer)
buf.ReadFrom(res.Body)
assert.Equal(t, "not a real jpeg", buf.String())
}
}

type nonJsonable struct {
}

Expand Down

0 comments on commit cdc73eb

Please sign in to comment.