Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 1509363

Browse files
authored
Merge pull request #30 from philips-software/feature/audit
HSDP Audit support
2 parents bb7be3d + 13ea648 commit 1509363

15 files changed

+838
-7
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ The current implement covers only a subset of HSDP APIs. Basically we implement
2828
- [x] MFA Policies
2929
- [x] Password Policies
3030
- [x] Logging ([examples](logging/README.md))
31-
- [ ] Auditing
31+
- [x] Auditing ([examples](audit/README.md))
3232
- [x] Clinical Data Repository (CDR)
3333
- [x] Tenant Onboarding
3434
- [x] Subscription management

audit/README.md

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
## Using the audit client
2+
3+
```go
4+
package main
5+
6+
import (
7+
"fmt"
8+
"net/http"
9+
"time"
10+
11+
dstu2ct "github.com/google/fhir/go/proto/google/fhir/proto/dstu2/codes_go_proto"
12+
dstu2dt "github.com/google/fhir/go/proto/google/fhir/proto/dstu2/datatypes_go_proto"
13+
dstu2pb "github.com/google/fhir/go/proto/google/fhir/proto/dstu2/resources_go_proto"
14+
15+
"github.com/philips-software/go-hsdp-api/audit/helper/fhir/dstu2"
16+
17+
"github.com/philips-software/go-hsdp-api/audit"
18+
)
19+
20+
func main() {
21+
productKey := "xxx-your-key-here-xxx"
22+
now := time.Now()
23+
24+
client, err := audit.NewClient(http.DefaultClient, &audit.Config{
25+
SharedSecret: "secrethere",
26+
SharedKey: "keyhere",
27+
AuditBaseURL: "https://your-create-url-here.eu-west.philips-healthsuite.com",
28+
})
29+
if err != nil {
30+
fmt.Printf("Error: %v\n", err)
31+
return
32+
}
33+
event, err := dstu2.NewAuditEvent(productKey, "andy",
34+
dstu2.AddSourceExtensionUriValue("applicationName", "patientapp"),
35+
dstu2.AddParticipant(&dstu2pb.AuditEvent_Participant{
36+
UserId: &dstu2dt.Identifier{
37+
Value: &dstu2dt.String{Value: "[email protected]"},
38+
},
39+
Requestor: &dstu2dt.Boolean{Value: true},
40+
}),
41+
dstu2.WithEvent(&dstu2pb.AuditEvent_Event{
42+
Action: &dstu2ct.AuditEventActionCode{
43+
Value: dstu2ct.AuditEventActionCode_E,
44+
},
45+
DateTime: dstu2.DateTime(now),
46+
Type: &dstu2dt.Coding{
47+
System: &dstu2dt.Uri{Value: "http://hl7.org/fhir/ValueSet/audit-event-type"},
48+
Version: &dstu2dt.String{Value: "1"},
49+
Code: &dstu2dt.Code{Value: "11011"},
50+
Display: &dstu2dt.String{Value: fmt.Sprintf("Timestamp %v", now.String())},
51+
},
52+
Outcome: &dstu2ct.AuditEventOutcomeCode{
53+
Value: dstu2ct.AuditEventOutcomeCode_INVALID_UNINITIALIZED,
54+
},
55+
OutcomeDesc: &dstu2dt.String{Value: "Success"},
56+
}),
57+
dstu2.WithSourceIdentifier(&dstu2dt.Identifier{
58+
Value: &dstu2dt.String{Value: "[email protected]"},
59+
Type: &dstu2dt.CodeableConcept{
60+
Coding: []*dstu2dt.Coding{
61+
{
62+
System: &dstu2dt.Uri{Value: "http://hl7.org/fhir/ValueSet/identifier-type"},
63+
Code: &dstu2dt.Code{Value: "4"},
64+
Display: &dstu2dt.String{Value: "application server"},
65+
},
66+
},
67+
},
68+
}))
69+
70+
if err != nil {
71+
fmt.Printf("Error: %v\n", err)
72+
return
73+
}
74+
outcome, resp, err := client.CreateAuditEvent(event)
75+
if err != nil {
76+
fmt.Printf("Error: %v\n", err)
77+
}
78+
if resp == nil {
79+
fmt.Printf("response is nil\n")
80+
return
81+
}
82+
fmt.Printf("Audit result: %d\n", resp.StatusCode)
83+
if outcome != nil {
84+
fmt.Printf("Outcome: %v\n", outcome)
85+
}
86+
}
87+
```

audit/client.go

+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Package pki provides support for HSDP CDR service
2+
//
3+
// We only intent to support the CDR FHIR STU3 and newer with this library.
4+
package audit
5+
6+
import (
7+
"bytes"
8+
"encoding/json"
9+
"fmt"
10+
"io"
11+
"io/ioutil"
12+
"net/http"
13+
"net/http/httputil"
14+
"net/url"
15+
"os"
16+
"strings"
17+
18+
"go.elastic.co/apm/module/apmhttp"
19+
20+
signer "github.com/philips-software/go-hsdp-signer"
21+
22+
"github.com/google/fhir/go/jsonformat"
23+
)
24+
25+
const (
26+
libraryVersion = "0.29.0"
27+
userAgent = "go-hsdp-api/audit/" + libraryVersion
28+
APIVersion = "2"
29+
)
30+
31+
// OptionFunc is the function signature function for options
32+
type OptionFunc func(*http.Request) error
33+
34+
// Config contains the configuration of a client
35+
type Config struct {
36+
Region string
37+
Environment string
38+
// ProductKey is provided as part of Auditing onboarding
39+
ProductKey string
40+
// Tenant value is used to support multi tenancy with a single ProductKey
41+
Tenant string
42+
// AuditBaseURL is provided as part of Auditing onboarding
43+
AuditBaseURL string
44+
// SharedKey is the IAM API signing key
45+
SharedKey string
46+
// SharedSecret is the IAM API signing secret
47+
SharedSecret string
48+
TimeZone string
49+
DebugLog string
50+
}
51+
52+
// A Client manages communication with HSDP CDR API
53+
type Client struct {
54+
config *Config
55+
httpClient *http.Client
56+
auditStoreURL *url.URL
57+
58+
// User agent used when communicating with the HSDP IAM API.
59+
UserAgent string
60+
61+
ma *jsonformat.Marshaller
62+
um *jsonformat.Unmarshaller
63+
httpSigner *signer.Signer
64+
65+
debugFile *os.File
66+
}
67+
68+
// NewClient returns a new HSDP Audit API client. Configured console and IAM clients
69+
// must be provided as the underlying API requires tokens from respective services
70+
func NewClient(httpClient *http.Client, config *Config) (*Client, error) {
71+
return newClient(httpClient, config)
72+
}
73+
74+
func newClient(httpClient *http.Client, config *Config) (*Client, error) {
75+
var err error
76+
77+
if httpClient == nil {
78+
httpClient = apmhttp.WrapClient(http.DefaultClient)
79+
}
80+
81+
c := &Client{httpClient: httpClient, config: config, UserAgent: userAgent}
82+
if config.DebugLog != "" {
83+
var err error
84+
c.debugFile, err = os.OpenFile(config.DebugLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
85+
if err != nil {
86+
c.debugFile = nil
87+
}
88+
}
89+
c.httpSigner, err = signer.New(c.config.SharedKey, c.config.SharedSecret)
90+
if err != nil {
91+
return nil, fmt.Errorf("signer.New: %w", err)
92+
}
93+
ma, err := jsonformat.NewMarshaller(false, "", "", jsonformat.STU3)
94+
if err != nil {
95+
return nil, fmt.Errorf("cdr.NewClient create FHIR STU3 marshaller: %w", err)
96+
}
97+
c.ma = ma
98+
um, err := jsonformat.NewUnmarshaller(config.TimeZone, jsonformat.STU3)
99+
if err != nil {
100+
return nil, fmt.Errorf("cdr.NewClient create FHIR STU3 unmarshaller (timezone=[%s]): %w", config.TimeZone, err)
101+
}
102+
c.um = um
103+
c.SetAuditBaseURL(c.config.AuditBaseURL)
104+
105+
return c, nil
106+
}
107+
108+
// Close releases allocated resources of clients
109+
func (c *Client) Close() {
110+
if c.debugFile != nil {
111+
_ = c.debugFile.Close()
112+
c.debugFile = nil
113+
}
114+
}
115+
116+
// SetAuditBaseURL sets the FHIR store URL for API requests to a custom endpoint. urlStr
117+
// should always be specified with a trailing slash.
118+
func (c *Client) SetAuditBaseURL(urlStr string) error {
119+
if urlStr == "" {
120+
return ErrBaseURLCannotBeEmpty
121+
}
122+
// Make sure the given URL end with a slash
123+
if !strings.HasSuffix(urlStr, "/") {
124+
urlStr += "/"
125+
}
126+
var err error
127+
c.auditStoreURL, err = url.Parse(urlStr)
128+
return err
129+
}
130+
131+
// NewAuditRequest creates an new CDR Service API request. A relative URL path can be provided in
132+
// urlStr, in which case it is resolved relative to the base URL of the Client.
133+
// Relative URL paths should always be specified without a preceding slash. If
134+
// specified, the value pointed to by body is JSON encoded and included as the
135+
// request body.
136+
func (c *Client) NewAuditRequest(method, path string, bodyBytes []byte, options []OptionFunc) (*http.Request, error) {
137+
u := *c.auditStoreURL
138+
// Set the encoded opaque data
139+
u.Path = c.auditStoreURL.Path + path
140+
141+
req := &http.Request{
142+
Method: method,
143+
URL: &u,
144+
Proto: "HTTP/1.1",
145+
ProtoMajor: 1,
146+
ProtoMinor: 1,
147+
Header: make(http.Header),
148+
Host: u.Host,
149+
}
150+
151+
for _, fn := range options {
152+
if fn == nil {
153+
continue
154+
}
155+
156+
if err := fn(req); err != nil {
157+
return nil, err
158+
}
159+
}
160+
161+
if method == "POST" || method == "PUT" || method == "PATCH" {
162+
bodyReader := bytes.NewReader(bodyBytes)
163+
164+
u.RawQuery = ""
165+
req.Body = ioutil.NopCloser(bodyReader)
166+
req.ContentLength = int64(bodyReader.Len())
167+
}
168+
169+
req.Header.Set("Accept", "*/*")
170+
req.Header.Set("API-Version", APIVersion)
171+
req.Header.Set("Content-Type", "application/json")
172+
173+
if c.UserAgent != "" {
174+
req.Header.Set("User-Agent", c.UserAgent)
175+
}
176+
return req, nil
177+
}
178+
179+
// Response is a HSDP IAM API response. This wraps the standard http.Response
180+
// returned from HSDP IAM and provides convenient access to things like errors
181+
type Response struct {
182+
*http.Response
183+
}
184+
185+
// newResponse creates a new Response for the provided http.Response.
186+
func newResponse(r *http.Response) *Response {
187+
response := &Response{Response: r}
188+
return response
189+
}
190+
191+
// Do executes a http request. If v implements the io.Writer
192+
// interface, the raw response body will be written to v, without attempting to
193+
// first decode it.
194+
func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
195+
if c.debugFile != nil {
196+
dumped, _ := httputil.DumpRequest(req, true)
197+
out := fmt.Sprintf("[go-hsdp-api] --- Request start ---\n%s\n[go-hsdp-api] Request end ---\n", string(dumped))
198+
_, _ = c.debugFile.WriteString(out)
199+
}
200+
resp, err := c.httpClient.Do(req)
201+
if c.debugFile != nil && resp != nil {
202+
dumped, _ := httputil.DumpResponse(resp, true)
203+
out := fmt.Sprintf("[go-hsdp-api] --- Response start ---\n%s\n[go-hsdp-api] --- Response end ---\n", string(dumped))
204+
_, _ = c.debugFile.WriteString(out)
205+
}
206+
if err != nil {
207+
return nil, err
208+
}
209+
210+
response := newResponse(resp)
211+
212+
doErr := CheckResponse(resp)
213+
214+
if v != nil {
215+
defer resp.Body.Close() // Only close if we plan to read it
216+
if w, ok := v.(io.Writer); ok {
217+
_, err = io.Copy(w, resp.Body)
218+
} else {
219+
err = json.NewDecoder(resp.Body).Decode(v)
220+
}
221+
if err != nil {
222+
return response, err
223+
}
224+
}
225+
226+
return response, doErr
227+
}
228+
229+
// CheckResponse checks the API response for errors, and returns them if present.
230+
func CheckResponse(r *http.Response) error {
231+
switch r.StatusCode {
232+
case 200, 201, 202, 204, 304:
233+
return nil
234+
case 400:
235+
return ErrBadRequest
236+
}
237+
return ErrNonHttp20xResponse
238+
}

0 commit comments

Comments
 (0)