1package restapi 2 3import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "net/http/cookiejar" 9 "net/url" 10 "strings" 11) 12 13type HttpClientFactory func() *http.Client 14 15// BaseAPIResponse represents the most basic standard Centrify API response, 16// where Result itself is left as raw json 17type BaseAPIResponse struct { 18 Success bool `json:"success"` 19 Result json.RawMessage 20 Message string 21} 22 23type StringResponse struct { 24 BaseAPIResponse 25 Result string 26} 27 28type BoolResponse struct { 29 BaseAPIResponse 30 Result bool 31} 32 33// GenericMapResponse represents Centrify API responses where results are map[string]interface{}, 34// this type allows direct access to these without further decoding. 35type GenericMapResponse struct { 36 BaseAPIResponse 37 Result map[string]interface{} 38} 39 40type HttpError struct { 41 error // error type 42 StatusCode int // HTTP status 43} 44 45// BackendType is the type of backend that is being implemented 46type RestClientMode uint32 47 48// RestClient represents a stateful API client (cookies maintained between calls, single service etc) 49type RestClient struct { 50 Service string 51 Client *http.Client 52 Headers map[string]string 53 SourceHeader string 54} 55 56// GetNewRestClient creates a new RestClient for the specified endpoint. If a factory for creating 57// http.Client's is not provided, you'll get a new: &http.Client{}. 58func GetNewRestClient(service string, httpFactory HttpClientFactory) (*RestClient, error) { 59 jar, err := cookiejar.New(nil) 60 61 if err != nil { 62 return nil, err 63 } 64 65 // Munge on the service a little bit, force it to have no trailing / and always start with https:// 66 url, err := url.Parse(service) 67 if err != nil { 68 return nil, err 69 } 70 url.Scheme = "https" 71 url.Path = "" 72 73 client := &RestClient{} 74 client.Service = url.String() 75 if httpFactory != nil { 76 client.Client = httpFactory() 77 } else { 78 client.Client = &http.Client{} 79 } 80 client.Client.Jar = jar 81 client.Headers = make(map[string]string) 82 client.SourceHeader = "cloud-golang-sdk" 83 return client, nil 84} 85 86func (r *RestClient) CallRawAPI(method string, args map[string]interface{}) ([]byte, error) { 87 return r.postAndGetBody(method, args) 88} 89 90func (r *RestClient) CallBaseAPI(method string, args map[string]interface{}) (*BaseAPIResponse, error) { 91 body, err := r.postAndGetBody(method, args) 92 if err != nil { 93 return nil, err 94 } 95 return bodyToBaseAPIResponse(body) 96} 97 98func (r *RestClient) CallGenericMapAPI(method string, args map[string]interface{}) (*GenericMapResponse, error) { 99 body, err := r.postAndGetBody(method, args) 100 if err != nil { 101 return nil, err 102 } 103 return bodyToGenericMapResponse(body) 104} 105 106func (r *RestClient) CallStringAPI(method string, args map[string]interface{}) (*StringResponse, error) { 107 body, err := r.postAndGetBody(method, args) 108 if err != nil { 109 return nil, err 110 } 111 return bodyToStringResponse(body) 112} 113 114func (r *RestClient) CallBoolAPI(method string, args map[string]interface{}) (*BoolResponse, error) { 115 body, err := r.postAndGetBody(method, args) 116 if err != nil { 117 return nil, err 118 } 119 return bodyToBoolResponse(body) 120} 121 122func (r *RestClient) postAndGetBody(method string, args map[string]interface{}) ([]byte, error) { 123 service := strings.TrimSuffix(r.Service, "/") 124 method = strings.TrimPrefix(method, "/") 125 postdata := strings.NewReader(payloadFromMap(args)) 126 postreq, err := http.NewRequest("POST", service+"/"+method, postdata) 127 128 if err != nil { 129 return nil, err 130 } 131 132 postreq.Header.Add("Content-Type", "application/json") 133 postreq.Header.Add("X-CENTRIFY-NATIVE-CLIENT", "Yes") 134 postreq.Header.Add("X-CFY-SRC", r.SourceHeader) 135 136 for k, v := range r.Headers { 137 postreq.Header.Add(k, v) 138 } 139 140 httpresp, err := r.Client.Do(postreq) 141 if err != nil { 142 return nil, err 143 } 144 145 defer httpresp.Body.Close() 146 147 if httpresp.StatusCode == 200 { 148 return ioutil.ReadAll(httpresp.Body) 149 } 150 151 body, _ := ioutil.ReadAll(httpresp.Body) 152 return nil, &HttpError{error: fmt.Errorf("POST to %s failed with code %d, body: %s", method, httpresp.StatusCode, body), StatusCode: httpresp.StatusCode} 153} 154 155// This function converts a map[string]interface{} into json string 156func payloadFromMap(input map[string]interface{}) string { 157 if input != nil { 158 p, _ := json.Marshal(input) 159 return string(p) 160 } 161 162 return "" 163} 164 165func bodyToBaseAPIResponse(body []byte) (*BaseAPIResponse, error) { 166 reply := &BaseAPIResponse{} 167 err := json.Unmarshal(body, &reply) 168 if err != nil { 169 return nil, fmt.Errorf("Failed to unmarshal BaseApiResponse from HTTP response: %v", err) 170 } 171 return reply, nil 172} 173 174func bodyToGenericMapResponse(body []byte) (*GenericMapResponse, error) { 175 reply := &GenericMapResponse{} 176 err := json.Unmarshal(body, &reply) 177 if err != nil { 178 return nil, fmt.Errorf("Failed to unmarshal GenericMapResponse from HTTP response: %v", err) 179 } 180 return reply, nil 181} 182 183func bodyToStringResponse(body []byte) (*StringResponse, error) { 184 reply := &StringResponse{} 185 err := json.Unmarshal(body, &reply) 186 if err != nil { 187 return nil, fmt.Errorf("Failed to unmarshal StringResponse from HTTP response: %v", err) 188 } 189 return reply, nil 190} 191 192func bodyToBoolResponse(body []byte) (*BoolResponse, error) { 193 reply := &BoolResponse{} 194 err := json.Unmarshal(body, &reply) 195 if err != nil { 196 return nil, fmt.Errorf("Failed to unmarshal BoolResponse from HTTP response: %v", err) 197 } 198 return reply, nil 199} 200