Skip to content
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

Merged
merged 6 commits into from
Jun 5, 2019
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 == "" {
Copy link
Contributor

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 ?

Copy link
Contributor Author

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

var addNfo = make(map[string]string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nfo ? is it a typo for Info ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to check for empty string ?

Copy link
Contributor Author

@fperot74 fperot74 Jun 4, 2019

Choose a reason for hiding this comment

The 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
}
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenericResponse is HTTP oriented.
The intial purpose was to split it between an answer that can be translated into a specific kind of response, HTTP or gRPC, ... I know that we don't have it anymore but I think it was a good split of concerns.

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