1package cfclient
2
3import (
4	"encoding/json"
5	"fmt"
6	"io"
7	"io/ioutil"
8	"net/http"
9	"reflect"
10	"strings"
11
12	"github.com/Masterminds/semver"
13	"github.com/pkg/errors"
14)
15
16type SecGroupResponse struct {
17	Count     int                `json:"total_results"`
18	Pages     int                `json:"total_pages"`
19	NextUrl   string             `json:"next_url"`
20	Resources []SecGroupResource `json:"resources"`
21}
22
23type SecGroupCreateResponse struct {
24	Code        int    `json:"code"`
25	ErrorCode   string `json:"error_code"`
26	Description string `json:"description"`
27}
28
29type SecGroupResource struct {
30	Meta   Meta     `json:"metadata"`
31	Entity SecGroup `json:"entity"`
32}
33
34type SecGroup struct {
35	Guid              string          `json:"guid"`
36	Name              string          `json:"name"`
37	CreatedAt         string          `json:"created_at"`
38	UpdatedAt         string          `json:"updated_at"`
39	Rules             []SecGroupRule  `json:"rules"`
40	Running           bool            `json:"running_default"`
41	Staging           bool            `json:"staging_default"`
42	SpacesURL         string          `json:"spaces_url"`
43	StagingSpacesURL  string          `json:"staging_spaces_url"`
44	SpacesData        []SpaceResource `json:"spaces"`
45	StagingSpacesData []SpaceResource `json:"staging_spaces"`
46	c                 *Client
47}
48
49type SecGroupRule struct {
50	Protocol    string `json:"protocol"`
51	Ports       string `json:"ports,omitempty"`       //e.g. "4000-5000,9142"
52	Destination string `json:"destination"`           //CIDR Format
53	Description string `json:"description,omitempty"` //Optional description
54	Code        int    `json:"code"`                  // ICMP code
55	Type        int    `json:"type"`                  //ICMP type. Only valid if Protocol=="icmp"
56	Log         bool   `json:"log,omitempty"`         //If true, log this rule
57}
58
59var MinStagingSpacesVersion *semver.Version = getMinStagingSpacesVersion()
60
61func (c *Client) ListSecGroups() (secGroups []SecGroup, err error) {
62	requestURL := "/v2/security_groups?inline-relations-depth=1"
63	for requestURL != "" {
64		var secGroupResp SecGroupResponse
65		r := c.NewRequest("GET", requestURL)
66		resp, err := c.DoRequest(r)
67
68		if err != nil {
69			return nil, errors.Wrap(err, "Error requesting sec groups")
70		}
71		resBody, err := ioutil.ReadAll(resp.Body)
72		if err != nil {
73			return nil, errors.Wrap(err, "Error reading sec group response body")
74		}
75
76		err = json.Unmarshal(resBody, &secGroupResp)
77		if err != nil {
78			return nil, errors.Wrap(err, "Error unmarshaling sec group")
79		}
80
81		for _, secGroup := range secGroupResp.Resources {
82			secGroup.Entity.Guid = secGroup.Meta.Guid
83			secGroup.Entity.CreatedAt = secGroup.Meta.CreatedAt
84			secGroup.Entity.UpdatedAt = secGroup.Meta.UpdatedAt
85			secGroup.Entity.c = c
86			for i, space := range secGroup.Entity.SpacesData {
87				space.Entity.Guid = space.Meta.Guid
88				secGroup.Entity.SpacesData[i] = space
89			}
90			if len(secGroup.Entity.SpacesData) == 0 {
91				spaces, err := secGroup.Entity.ListSpaceResources()
92				if err != nil {
93					return nil, err
94				}
95				for _, space := range spaces {
96					secGroup.Entity.SpacesData = append(secGroup.Entity.SpacesData, space)
97				}
98			}
99			if len(secGroup.Entity.StagingSpacesData) == 0 {
100				spaces, err := secGroup.Entity.ListStagingSpaceResources()
101				if err != nil {
102					return nil, err
103				}
104				for _, space := range spaces {
105					secGroup.Entity.StagingSpacesData = append(secGroup.Entity.SpacesData, space)
106				}
107			}
108			secGroups = append(secGroups, secGroup.Entity)
109		}
110
111		requestURL = secGroupResp.NextUrl
112		resp.Body.Close()
113	}
114	return secGroups, nil
115}
116
117func (c *Client) ListRunningSecGroups() ([]SecGroup, error) {
118	secGroups := make([]SecGroup, 0)
119	requestURL := "/v2/config/running_security_groups"
120	for requestURL != "" {
121		var secGroupResp SecGroupResponse
122		r := c.NewRequest("GET", requestURL)
123		resp, err := c.DoRequest(r)
124
125		if err != nil {
126			return nil, errors.Wrap(err, "Error requesting sec groups")
127		}
128		resBody, err := ioutil.ReadAll(resp.Body)
129		if err != nil {
130			return nil, errors.Wrap(err, "Error reading sec group response body")
131		}
132
133		err = json.Unmarshal(resBody, &secGroupResp)
134		if err != nil {
135			return nil, errors.Wrap(err, "Error unmarshaling sec group")
136		}
137
138		for _, secGroup := range secGroupResp.Resources {
139			secGroup.Entity.Guid = secGroup.Meta.Guid
140			secGroup.Entity.CreatedAt = secGroup.Meta.CreatedAt
141			secGroup.Entity.UpdatedAt = secGroup.Meta.UpdatedAt
142			secGroup.Entity.c = c
143
144			secGroups = append(secGroups, secGroup.Entity)
145		}
146
147		requestURL = secGroupResp.NextUrl
148		resp.Body.Close()
149	}
150	return secGroups, nil
151}
152
153func (c *Client) ListStagingSecGroups() ([]SecGroup, error) {
154	secGroups := make([]SecGroup, 0)
155	requestURL := "/v2/config/staging_security_groups"
156	for requestURL != "" {
157		var secGroupResp SecGroupResponse
158		r := c.NewRequest("GET", requestURL)
159		resp, err := c.DoRequest(r)
160
161		if err != nil {
162			return nil, errors.Wrap(err, "Error requesting sec groups")
163		}
164		resBody, err := ioutil.ReadAll(resp.Body)
165		if err != nil {
166			return nil, errors.Wrap(err, "Error reading sec group response body")
167		}
168
169		err = json.Unmarshal(resBody, &secGroupResp)
170		if err != nil {
171			return nil, errors.Wrap(err, "Error unmarshaling sec group")
172		}
173
174		for _, secGroup := range secGroupResp.Resources {
175			secGroup.Entity.Guid = secGroup.Meta.Guid
176			secGroup.Entity.CreatedAt = secGroup.Meta.CreatedAt
177			secGroup.Entity.UpdatedAt = secGroup.Meta.UpdatedAt
178			secGroup.Entity.c = c
179
180			secGroups = append(secGroups, secGroup.Entity)
181		}
182
183		requestURL = secGroupResp.NextUrl
184		resp.Body.Close()
185	}
186	return secGroups, nil
187}
188
189func (c *Client) GetSecGroupByName(name string) (secGroup SecGroup, err error) {
190	requestURL := "/v2/security_groups?q=name:" + name
191	var secGroupResp SecGroupResponse
192	r := c.NewRequest("GET", requestURL)
193	resp, err := c.DoRequest(r)
194
195	if err != nil {
196		return secGroup, errors.Wrap(err, "Error requesting sec groups")
197	}
198	resBody, err := ioutil.ReadAll(resp.Body)
199	if err != nil {
200		return secGroup, errors.Wrap(err, "Error reading sec group response body")
201	}
202
203	err = json.Unmarshal(resBody, &secGroupResp)
204	if err != nil {
205		return secGroup, errors.Wrap(err, "Error unmarshaling sec group")
206	}
207	if len(secGroupResp.Resources) == 0 {
208		return secGroup, fmt.Errorf("No security group with name %v found", name)
209	}
210	secGroup = secGroupResp.Resources[0].Entity
211	secGroup.Guid = secGroupResp.Resources[0].Meta.Guid
212	secGroup.CreatedAt = secGroupResp.Resources[0].Meta.CreatedAt
213	secGroup.UpdatedAt = secGroupResp.Resources[0].Meta.UpdatedAt
214	secGroup.c = c
215
216	resp.Body.Close()
217	return secGroup, nil
218}
219
220func (secGroup *SecGroup) ListSpaceResources() ([]SpaceResource, error) {
221	var spaceResources []SpaceResource
222	requestURL := secGroup.SpacesURL
223	for requestURL != "" {
224		spaceResp, err := secGroup.c.getSpaceResponse(requestURL)
225		if err != nil {
226			return []SpaceResource{}, err
227		}
228		for i, spaceRes := range spaceResp.Resources {
229			spaceRes.Entity.Guid = spaceRes.Meta.Guid
230			spaceRes.Entity.CreatedAt = spaceRes.Meta.CreatedAt
231			spaceRes.Entity.UpdatedAt = spaceRes.Meta.UpdatedAt
232			spaceResp.Resources[i] = spaceRes
233		}
234		spaceResources = append(spaceResources, spaceResp.Resources...)
235		requestURL = spaceResp.NextUrl
236	}
237	return spaceResources, nil
238}
239
240func (secGroup *SecGroup) ListStagingSpaceResources() ([]SpaceResource, error) {
241	var spaceResources []SpaceResource
242	requestURL := secGroup.StagingSpacesURL
243	for requestURL != "" {
244		spaceResp, err := secGroup.c.getSpaceResponse(requestURL)
245		if err != nil {
246			// if this is a 404, let's make sure that it's not because we're on a legacy system
247			if cause := errors.Cause(err); cause != nil {
248				if httpErr, ok := cause.(CloudFoundryHTTPError); ok {
249					if httpErr.StatusCode == 404 {
250						info, infoErr := secGroup.c.GetInfo()
251						if infoErr != nil {
252							return nil, infoErr
253						}
254
255						apiVersion, versionErr := semver.NewVersion(info.APIVersion)
256						if versionErr != nil {
257							return nil, versionErr
258						}
259
260						if MinStagingSpacesVersion.GreaterThan(apiVersion) {
261							// this is probably not really an error, we're just trying to use a non-existent api
262							return nil, nil
263						}
264					}
265				}
266			}
267
268			return []SpaceResource{}, err
269		}
270		for i, spaceRes := range spaceResp.Resources {
271			spaceRes.Entity.Guid = spaceRes.Meta.Guid
272			spaceRes.Entity.CreatedAt = spaceRes.Meta.CreatedAt
273			spaceRes.Entity.UpdatedAt = spaceRes.Meta.UpdatedAt
274			spaceResp.Resources[i] = spaceRes
275		}
276		spaceResources = append(spaceResources, spaceResp.Resources...)
277		requestURL = spaceResp.NextUrl
278	}
279	return spaceResources, nil
280}
281
282/*
283CreateSecGroup contacts the CF endpoint for creating a new security group.
284name: the name to give to the created security group
285rules: A slice of rule objects that describe the rules that this security group enforces.
286	This can technically be nil or an empty slice - we won't judge you
287spaceGuids: The security group will be associated with the spaces specified by the contents of this slice.
288	If nil, the security group will not be associated with any spaces initially.
289*/
290func (c *Client) CreateSecGroup(name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) {
291	return c.secGroupCreateHelper("/v2/security_groups", "POST", name, rules, spaceGuids)
292}
293
294/*
295UpdateSecGroup contacts the CF endpoint to update an existing security group.
296guid: identifies the security group that you would like to update.
297name: the new name to give to the security group
298rules: A slice of rule objects that describe the rules that this security group enforces.
299	If this is left nil, the rules will not be changed.
300spaceGuids: The security group will be associated with the spaces specified by the contents of this slice.
301	If nil, the space associations will not be changed.
302*/
303func (c *Client) UpdateSecGroup(guid, name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) {
304	return c.secGroupCreateHelper("/v2/security_groups/"+guid, "PUT", name, rules, spaceGuids)
305}
306
307/*
308DeleteSecGroup contacts the CF endpoint to delete an existing security group.
309guid: Indentifies the security group to be deleted.
310*/
311func (c *Client) DeleteSecGroup(guid string) error {
312	//Perform the DELETE and check for errors
313	resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/security_groups/%s", guid)))
314	if err != nil {
315		return err
316	}
317	if resp.StatusCode != 204 { //204 No Content
318		return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
319	}
320	return nil
321}
322
323/*
324GetSecGroup contacts the CF endpoint for fetching the info for a particular security group.
325guid: Identifies the security group to fetch information from
326*/
327func (c *Client) GetSecGroup(guid string) (*SecGroup, error) {
328	//Perform the GET and check for errors
329	resp, err := c.DoRequest(c.NewRequest("GET", "/v2/security_groups/"+guid))
330	if err != nil {
331		return nil, err
332	}
333	if resp.StatusCode != 200 {
334		return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
335	}
336	//get the json out of the response body
337	return respBodyToSecGroup(resp.Body, c)
338}
339
340/*
341BindSecGroup contacts the CF endpoint to associate a space with a security group
342secGUID: identifies the security group to add a space to
343spaceGUID: identifies the space to associate
344*/
345func (c *Client) BindSecGroup(secGUID, spaceGUID string) error {
346	//Perform the PUT and check for errors
347	resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/security_groups/%s/spaces/%s", secGUID, spaceGUID)))
348	if err != nil {
349		return err
350	}
351	if resp.StatusCode != 201 { //201 Created
352		return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
353	}
354	return nil
355}
356
357/*
358BindSpaceStagingSecGroup contacts the CF endpoint to associate a space with a security group for staging functions only
359secGUID: identifies the security group to add a space to
360spaceGUID: identifies the space to associate
361*/
362func (c *Client) BindStagingSecGroupToSpace(secGUID, spaceGUID string) error {
363	//Perform the PUT and check for errors
364	resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/security_groups/%s/staging_spaces/%s", secGUID, spaceGUID)))
365	if err != nil {
366		return err
367	}
368	if resp.StatusCode != 201 { //201 Created
369		return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
370	}
371	return nil
372}
373
374/*
375BindRunningSecGroup contacts the CF endpoint to associate  a security group
376secGUID: identifies the security group to add a space to
377*/
378func (c *Client) BindRunningSecGroup(secGUID string) error {
379	//Perform the PUT and check for errors
380	resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/config/running_security_groups/%s", secGUID)))
381	if err != nil {
382		return err
383	}
384	if resp.StatusCode != 200 { //200
385		return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
386	}
387	return nil
388}
389
390/*
391UnbindRunningSecGroup contacts the CF endpoint to dis-associate  a security group
392secGUID: identifies the security group to add a space to
393*/
394func (c *Client) UnbindRunningSecGroup(secGUID string) error {
395	//Perform the DELETE and check for errors
396	resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/config/running_security_groups/%s", secGUID)))
397	if err != nil {
398		return err
399	}
400	if resp.StatusCode != http.StatusNoContent { //204
401		return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
402	}
403	return nil
404}
405
406/*
407BindStagingSecGroup contacts the CF endpoint to associate a space with a security group
408secGUID: identifies the security group to add a space to
409*/
410func (c *Client) BindStagingSecGroup(secGUID string) error {
411	//Perform the PUT and check for errors
412	resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/config/staging_security_groups/%s", secGUID)))
413	if err != nil {
414		return err
415	}
416	if resp.StatusCode != 200 { //200
417		return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
418	}
419	return nil
420}
421
422/*
423UnbindStagingSecGroup contacts the CF endpoint to dis-associate a space with a security group
424secGUID: identifies the security group to add a space to
425*/
426func (c *Client) UnbindStagingSecGroup(secGUID string) error {
427	//Perform the DELETE and check for errors
428	resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/config/staging_security_groups/%s", secGUID)))
429	if err != nil {
430		return err
431	}
432	if resp.StatusCode != http.StatusNoContent { //204
433		return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
434	}
435	return nil
436}
437
438/*
439UnbindSecGroup contacts the CF endpoint to dissociate a space from a security group
440secGUID: identifies the security group to remove a space from
441spaceGUID: identifies the space to dissociate from the security group
442*/
443func (c *Client) UnbindSecGroup(secGUID, spaceGUID string) error {
444	//Perform the DELETE and check for errors
445	resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/security_groups/%s/spaces/%s", secGUID, spaceGUID)))
446	if err != nil {
447		return err
448	}
449	if resp.StatusCode != 204 { //204 No Content
450		return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
451	}
452	return nil
453}
454
455//Reads most security group response bodies into a SecGroup object
456func respBodyToSecGroup(body io.ReadCloser, c *Client) (*SecGroup, error) {
457	//get the json from the response body
458	bodyRaw, err := ioutil.ReadAll(body)
459	if err != nil {
460		return nil, errors.Wrap(err, "Could not read response body")
461	}
462	jStruct := SecGroupResource{}
463	//make it a SecGroup
464	err = json.Unmarshal(bodyRaw, &jStruct)
465	if err != nil {
466		return nil, errors.Wrap(err, "Could not unmarshal response body as json")
467	}
468	//pull a few extra fields from other places
469	ret := jStruct.Entity
470	ret.Guid = jStruct.Meta.Guid
471	ret.CreatedAt = jStruct.Meta.CreatedAt
472	ret.UpdatedAt = jStruct.Meta.UpdatedAt
473	ret.c = c
474	return &ret, nil
475}
476
477func convertStructToMap(st interface{}) map[string]interface{} {
478	reqRules := make(map[string]interface{})
479
480	v := reflect.ValueOf(st)
481	t := reflect.TypeOf(st)
482
483	for i := 0; i < v.NumField(); i++ {
484		key := strings.ToLower(t.Field(i).Name)
485		typ := v.FieldByName(t.Field(i).Name).Kind().String()
486		structTag := t.Field(i).Tag.Get("json")
487		jsonName := strings.TrimSpace(strings.Split(structTag, ",")[0])
488		value := v.FieldByName(t.Field(i).Name)
489
490		// if jsonName is not empty use it for the key
491		if jsonName != "" {
492			key = jsonName
493		}
494
495		if typ == "string" {
496			if !(value.String() == "" && strings.Contains(structTag, "omitempty")) {
497				reqRules[key] = value.String()
498			}
499		} else if typ == "int" {
500			reqRules[key] = value.Int()
501		} else {
502			reqRules[key] = value.Interface()
503		}
504
505	}
506
507	return reqRules
508}
509
510//Create and Update secGroup pretty much do the same thing, so this function abstracts those out.
511func (c *Client) secGroupCreateHelper(url, method, name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) {
512	reqRules := make([]map[string]interface{}, len(rules))
513
514	for i, rule := range rules {
515		reqRules[i] = convertStructToMap(rule)
516		protocol := strings.ToLower(reqRules[i]["protocol"].(string))
517
518		// if not icmp protocol need to remove the Code/Type fields
519		if protocol != "icmp" {
520			delete(reqRules[i], "code")
521			delete(reqRules[i], "type")
522		}
523	}
524
525	req := c.NewRequest(method, url)
526	//set up request body
527	inputs := map[string]interface{}{
528		"name":  name,
529		"rules": reqRules,
530	}
531
532	if spaceGuids != nil {
533		inputs["space_guids"] = spaceGuids
534	}
535	req.obj = inputs
536	//fire off the request and check for problems
537	resp, err := c.DoRequest(req)
538	if err != nil {
539		return nil, err
540	}
541	if resp.StatusCode != 201 { // Both create and update should give 201 CREATED
542		var response SecGroupCreateResponse
543
544		bodyRaw, _ := ioutil.ReadAll(resp.Body)
545
546		err = json.Unmarshal(bodyRaw, &response)
547		if err != nil {
548			return nil, errors.Wrap(err, "Error unmarshaling response")
549		}
550
551		return nil, fmt.Errorf(`Request failed CF API returned with status code %d
552-------------------------------
553Error Code  %s
554Code        %d
555Description %s`,
556			resp.StatusCode, response.ErrorCode, response.Code, response.Description)
557	}
558	//get the json from the response body
559	return respBodyToSecGroup(resp.Body, c)
560}
561
562func getMinStagingSpacesVersion() *semver.Version {
563	v, _ := semver.NewVersion("2.68.0")
564	return v
565}
566