diff --git a/docs/source/api/capabilities.rst b/docs/source/api/capabilities.rst index 2f61110e27..821ccaf6ea 100644 --- a/docs/source/api/capabilities.rst +++ b/docs/source/api/capabilities.rst @@ -29,41 +29,64 @@ Get all capabilities. Request Structure ----------------- -No available parameters +.. table:: Request Query Parameters + + +-----------+----------+---------------------------------------------------------------------------------------------------------------------+ + | Name | Required | Description | + +===========+==========+=====================================================================================================================+ + | name | no | Return only the capability that has this name | + +-----------+----------+---------------------------------------------------------------------------------------------------------------------+ + | orderby | no | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response`` array | + +-----------+----------+---------------------------------------------------------------------------------------------------------------------+ + | sortOrder | no | Changes the order of sorting. Either ascending (default or "asc") or descending ("desc") | + +-----------+----------+---------------------------------------------------------------------------------------------------------------------+ + | limit | no | Choose the maximum number of results to return | + +-----------+----------+---------------------------------------------------------------------------------------------------------------------+ + | offset | no | The number of results to skip before beginning to return results. Must use in conjunction with ``limit`` | + +-----------+----------+---------------------------------------------------------------------------------------------------------------------+ + | page | no | Return the n\ :sup:`th` page of results, where "n" is the value of this parameter, pages are ``limit`` long and the | + | | | first page is 1. If ``offset`` was defined, this query parameter has no effect. ``limit`` must be defined to make | + | | | use of ``page``. | + +-----------+----------+---------------------------------------------------------------------------------------------------------------------+ + + +.. code-block:: http + :caption: Request Example + + GET /api/1.4/capabilities?name=test HTTP/1.1 + Host: trafficops.infra.ciab.test + User-Agent: curl/7.47.0 + Accept: */* + Cookie: mojolicious=... Response Structure ------------------ :name: Name of the capability -:description: Describes the APIs covered by the capability. -:lastUpdated: Date and time of the last update made to this capability, in ISO format +:description: Describes the permissions covered by the capability. +:lastUpdated: Date and time of the last update made to this capability, in an ISO-like format .. code-block:: http :caption: Response Example HTTP/1.1 200 OK Access-Control-Allow-Credentials: true - Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept + Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE Access-Control-Allow-Origin: * - Cache-Control: no-cache, no-store, max-age=0, must-revalidate Content-Type: application/json - Date: Wed, 14 Nov 2018 20:26:19 GMT - Server: Mojolicious (Perl) - Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly Vary: Accept-Encoding - Whole-Content-Sha512: zmjsQO3Y4r1/xCFOHB+E+8+bbgDyVcvoR0d4gKqqsWTFaUnxp2flIzuFqWjXf+wb4Bbd1e2Ojse4nQKnyIFKGw== Transfer-Encoding: chunked + Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly + Whole-Content-Sha512: c18+GtX2ZI8PoCSwuAzBhl+6w3vDpKQTa/cDJC0WHxdpguOL378KBxGWW5PCSyZfJUb7wPyOL5qKMn6NNTufhg== + X-Server-Name: traffic_ops_golang/ + Date: Thu, 15 Aug 2019 17:20:20 GMT + Content-Length: 161 { "response": [ { - "name": "cdn-read", - "description": "View CDN configuration", - "lastUpdated": "2017-04-02 08:22:43" - }, - { - "name": "cdn-write", - "description": "Create, edit or delete CDN configuration", - "lastUpdated": "2017-04-02 08:22:43" + "description": "This is only a test. If this were a real capability, it might do something", + "lastUpdated": "2019-08-15 17:18:03+00", + "name": "test" } ]} @@ -89,17 +112,18 @@ Request Structure User-Agent: curl/7.47.0 Accept: */* Cookie: mojolicious=... - Content-Length: 109 + Content-Length: 73 Content-Type: application/json { - "name": "test", - "description": "This is only a test. If this were a real capability, it might do something" + "name": "testquest", + "description": "A test capability for API examples" } Response Structure ------------------ -:description: Describes the APIs covered by the capability. +:description: Describes the permissions covered by the capability. +:lastUpdated: Date and time of the last update made to this capability, in an ISO-like format :name: Name of the capability .. code-block:: http @@ -107,26 +131,28 @@ Response Structure HTTP/1.1 200 OK Access-Control-Allow-Credentials: true - Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept + Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE Access-Control-Allow-Origin: * - Cache-Control: no-cache, no-store, max-age=0, must-revalidate + Content-Encoding: gzip Content-Type: application/json - Date: Wed, 14 Nov 2018 20:33:00 GMT - Server: Mojolicious (Perl) - Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly - Vary: Accept-Encoding - Whole-Content-Sha512: HhhQzw3JBLv90lOeeSGj75uknADanz3fUnQt1E266HAKPTFuTjuIJpf8ni9fb9Chv9LN7mt16utcHMbP8MBHZw== - Content-Length: 183 + Set-Cookie: mojolicious=...; Path=/; Expires=Tue, 07 Jan 2020 20:06:18 GMT; Max-Age=3600; HttpOnly + X-Server-Name: traffic_ops_golang/ + Date: Tue, 07 Jan 2020 19:06:18 GMT + Content-Length: 225 { "alerts": [ { - "level": "success", - "text": "Capability was created." + "text": "Capability created.", + "level": "success" + }, + { + "text": "This endpoint is deprecated, and will be removed in the future", + "level": "warning" } ], "response": { - "name": "test", - "description": "This is only a test. If this were a real capability, it might do something" + "description": "A test capability for API examples", + "lastUpdated": "2020-01-07 19:06:18+00", + "name": "testquest" }} - diff --git a/lib/go-tc/capabilities.go b/lib/go-tc/capabilities.go new file mode 100644 index 0000000000..c520e503ff --- /dev/null +++ b/lib/go-tc/capabilities.go @@ -0,0 +1,38 @@ +package tc + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Capability reflects the ability of a user in ATC to perform some operation. +// +// In practice, they are assigned to relevant Traffic Ops API endpoints - to describe the +// capabilites of said endpoint - and to user permission Roles - to describe the capabilities +// afforded by said Role. Note that enforcement of Capability-based permisions is not currently +// implemented. +type Capability struct { + Description string `json:"description" db:"description"` + LastUpdated TimeNoMod `json:"lastUpdated" db:"last_updated"` + Name string `json:"name" db:"name"` +} + +// CapabilitiesResponse models the structure of a minimal response from the Capabilities API in +// Traffic Ops. +type CapabilitiesResponse struct { + Response []Capability `json:"response"` +} diff --git a/traffic_control/clients/python/trafficops/tosession.py b/traffic_control/clients/python/trafficops/tosession.py index 00590ac11a..0b1d22fe95 100644 --- a/traffic_control/clients/python/trafficops/tosession.py +++ b/traffic_control/clients/python/trafficops/tosession.py @@ -540,6 +540,31 @@ def get_cache_stats(self, query_params=None): :raises: Union[LoginError, OperationError] """ + # + # Capabilities + # + @api_request(u'get', u'capabilities', (u'1.1', u'1.2', u'1.3', u'1.4')) + def get_capabilities(self, query_params=None): + """ + Retrieves capabilities + :ref:`to-api-capabilities` + :param query_params: See API page for more information on accepted parameters + :type query_params: Dict[str, Any] + :rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], requests.Response] + :raises: Union[LoginError, OperationError] + """ + + @api_request(u'post', u'capabilities', (u'1.1', u'1.2', u'1.3', u'1.4')) + def create_capability(self, data=None): + """ + Creates a capability + :ref:`to-api-capabilities` + :param data: See API page for more information on accepted request body data + :type data: Any + :rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], requests.Response] + :raises: Union[LoginError, OperationError] + """ + # # CDN # diff --git a/traffic_ops/client/capability.go b/traffic_ops/client/capability.go new file mode 100644 index 0000000000..100c0fab29 --- /dev/null +++ b/traffic_ops/client/capability.go @@ -0,0 +1,84 @@ +package client + +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import "encoding/json" +import "errors" +import "net" +import "net/http" +import "net/url" + +import "github.com/apache/trafficcontrol/lib/go-tc" + +const API_v14_CAPABILITIES = "/api/1.4/capabilities" + +// CreateCapability creates the passed capability. +func (to *Session) CreateCapability(c tc.Capability) (tc.Alerts, ReqInf, error) { + var remoteAddr net.Addr + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + + reqBody, err := json.Marshal(c) + if err != nil { + return tc.Alerts{}, reqInf, err + } + + resp, remoteAddr, err := to.request(http.MethodPost, API_v14_CAPABILITIES, reqBody) + if err != nil { + return tc.Alerts{}, reqInf, err + } + defer resp.Body.Close() + reqInf.RemoteAddr = remoteAddr + + var alerts tc.Alerts + err = json.NewDecoder(resp.Body).Decode(&alerts) + return alerts, reqInf, err +} + +// GetCapabilities retrieves all capabilities. +func (to *Session) GetCapabilities() ([]tc.Capability, ReqInf, error) { + resp, remoteAddr, err := to.request(http.MethodGet, API_v14_CAPABILITIES, nil) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return nil, reqInf, err + } + defer resp.Body.Close() + + var data tc.CapabilitiesResponse + err = json.NewDecoder(resp.Body).Decode(&data) + return data.Response, reqInf, err +} + +// GetCapability retrieves only the capability named 'c' +func (to *Session) GetCapability(c string) (tc.Capability, ReqInf, error) { + var v url.Values + v.Add("name", c) + endpoint := API_v14_CAPABILITIES + "?" + v.Encode() + resp, remoteAddr, err := to.request(http.MethodGet, endpoint, nil) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return tc.Capability{}, reqInf, err + } + defer resp.Body.Close() + + var data tc.CapabilitiesResponse + err = json.NewDecoder(resp.Body).Decode(&data) + if err != nil { + return tc.Capability{}, reqInf, err + } else if data.Response == nil || len(data.Response) < 1 { + return tc.Capability{}, reqInf, errors.New("Invalid response - no capability returned!") + } + + return data.Response[0], reqInf, nil +} diff --git a/traffic_ops/testing/api/v1/tc-fixtures.json b/traffic_ops/testing/api/v1/tc-fixtures.json index ff8f06081f..be9210d854 100644 --- a/traffic_ops/testing/api/v1/tc-fixtures.json +++ b/traffic_ops/testing/api/v1/tc-fixtures.json @@ -2616,5 +2616,15 @@ "statValue": 1000, "summaryTime": "2019-01-01T00:00:00-06:00" } + ], + "capabilities": [ + { + "name": "test", + "description": "quest" + }, + { + "name": "foo", + "description": "bar" + } ] } diff --git a/traffic_ops/testing/api/v1/todb_test.go b/traffic_ops/testing/api/v1/todb_test.go index e8e045100e..e1b40e5fcc 100644 --- a/traffic_ops/testing/api/v1/todb_test.go +++ b/traffic_ops/testing/api/v1/todb_test.go @@ -68,6 +68,12 @@ func SetupTestData(*sql.DB) error { os.Exit(1) } + err = SetupAPICapabilities(db) + if err != nil { + fmt.Printf("\nError setting up APICapabilities %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err) + os.Exit(1) + } + err = SetupTenants(db) if err != nil { fmt.Printf("\nError setting up tenant %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err) @@ -131,6 +137,9 @@ func SetupCapabilities(db *sql.DB) error { INSERT INTO capability (name, description) VALUES ('all-read','Full read access') ON CONFLICT DO NOTHING; INSERT INTO capability (name, description) VALUES ('all-write','Full write access') ON CONFLICT DO NOTHING; INSERT INTO capability (name, description) VALUES ('cdn-read','View CDN configuration') ON CONFLICT DO NOTHING; +INSERT INTO capability (name, description) VALUES ('asns-read', 'Read ASNs') ON CONFLICT DO NOTHING; +INSERT INTO capability (name, description) VALUES ('asns-write', 'Write ASNs') ON CONFLICT DO NOTHING; +INSERT INTO capability (name, description) VALUES ('cache-groups-read', 'Read CGs') ON CONFLICT DO NOTHING; ` err := execSQL(db, sqlStmt, "capability") if err != nil { @@ -139,6 +148,20 @@ INSERT INTO capability (name, description) VALUES ('cdn-read','View CDN configur return nil } +func SetupAPICapabilities(db *sql.DB) error { + sqlStmt := ` +INSERT INTO api_capability (http_method, route, capability) VALUES ('GET', '/asns', 'asns-read') ON CONFLICT DO NOTHING; +INSERT INTO api_capability (http_method, route, capability) VALUES ('POST', '/asns', 'asns-write') ON CONFLICT DO NOTHING; +INSERT INTO api_capability (http_method, route, capability) VALUES ('GET', '/cachegroups', 'cache-groups-read') ON CONFLICT DO NOTHING; +` + + err := execSQL(db, sqlStmt, "api_capability") + if err != nil { + return fmt.Errorf("exec failed %v", err) + } + return nil +} + func SetupRoleCapabilities(db *sql.DB) error { sqlStmt := ` INSERT INTO role_capability (role_id, cap_name) VALUES (4,'all-write') ON CONFLICT DO NOTHING; @@ -280,6 +303,7 @@ INSERT INTO to_extension (name, version, info_url, isactive, script_file, server func Teardown(db *sql.DB) error { sqlStmt := ` + DELETE FROM api_capability; DELETE FROM deliveryservices_required_capability; DELETE FROM server_server_capability; DELETE FROM server_server_capability; @@ -294,6 +318,7 @@ func Teardown(db *sql.DB) error { DELETE FROM deliveryservice_tmuser; DELETE FROM tm_user; DELETE FROM role; + DELETE FROM capability; ALTER SEQUENCE role_id_seq RESTART WITH 1; DELETE FROM deliveryservice_regex; DELETE FROM regex; diff --git a/traffic_ops/testing/api/v1/traffic_control_test.go b/traffic_ops/testing/api/v1/traffic_control_test.go index 4a419891ec..bc4f3b1f50 100644 --- a/traffic_ops/testing/api/v1/traffic_control_test.go +++ b/traffic_ops/testing/api/v1/traffic_control_test.go @@ -25,6 +25,7 @@ type TrafficControl struct { CDNs []tc.CDN `json:"cdns"` CacheGroups []tc.CacheGroupNullable `json:"cachegroups"` CacheGroupParameterRequests []tc.CacheGroupParameterRequest `json:"cachegroupParameters"` + Capabilities []tc.Capability `json:"capability"` Coordinates []tc.Coordinate `json:"coordinates"` DeliveryServiceRequests []tc.DeliveryServiceRequest `json:"deliveryServiceRequests"` DeliveryServiceRequestComments []tc.DeliveryServiceRequestComment `json:"deliveryServiceRequestComments"` diff --git a/traffic_ops/testing/api/v14/capabilities_test.go b/traffic_ops/testing/api/v14/capabilities_test.go new file mode 100644 index 0000000000..4f2008f113 --- /dev/null +++ b/traffic_ops/testing/api/v14/capabilities_test.go @@ -0,0 +1,119 @@ +package v14 + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import "testing" + +import "github.com/apache/trafficcontrol/lib/go-log" +import "github.com/apache/trafficcontrol/lib/go-tc" + +// These capabilities are defined during the setup process in todb.go. +// ANY TIME THOSE ARE CHANGED THIS MUST BE UPDATED. +var staticCapabilities = []tc.Capability { + tc.Capability{ + Name: "all-read", + Description: "Full read access", + }, + tc.Capability { + Name: "all-write", + Description: "Full write access", + }, + tc.Capability { + Name: "cdn-read", + Description: "View CDN configuration", + }, + tc.Capability { + Name: "asns-read", + Description: "Read ASNs", + }, + tc.Capability { + Name: "asns-write", + Description: "Write ASNs", + }, + tc.Capability { + Name: "cache-groups-read", + Description: "Read CGs", + }, +} + +func TestCapabilities(t *testing.T) { + CreateTestCapabilities(t) + GetTestCapabilities(t) +} + +func CreateTestCapabilities(t *testing.T) { + for _,c := range testData.Capabilities { + resp, _, err := TOSession.CreateCapability(c) + log.Debugln("Response: ", c.Name, " ", resp) + if err != nil { + t.Errorf("could not create capability: %v", err) + } + } +} + +func GetTestCapabilities(t *testing.T) { + testDataLen := len(testData.Capabilities) + len(staticCapabilities) + capMap := make(map[string]string, testDataLen) + + for _,c := range testData.Capabilities { + capMap[c.Name] = c.Description + cap, _, err := TOSession.GetCapability(c.Name) + if err != nil { + t.Errorf("could not get capability '%s': %v", c.Name, err) + continue + } + + if cap.Name != c.Name { + t.Errorf("requested capacity '%s' but got a capacity with the name '%s'", c.Name, cap.Name) + } + if cap.Description != c.Description { + t.Errorf("capacity '%s' has the wrong description, want '%s' but got '%s'", c.Name, c.Description, cap.Description) + } + } + + // Hopefully this won't need to be done for much longer + for _,c := range staticCapabilities { + capMap[c.Name] = c.Description + } + + + caps, _, err := TOSession.GetCapabilities() + if err != nil { + t.Fatalf("could not get all capabilities: %v", err) + } + if len(caps) != testDataLen { + t.Fatalf("response returned different number of capabilities than those that exist; got %d, want %d", len(caps), testDataLen) + } + + for _,c := range caps { + if desc, ok := capMap[c.Name]; !ok { + t.Errorf("capability '%s' found in response, but not in test data!", c.Name) + } else { + if desc != c.Description { + t.Errorf("capability '%s' has description '%s' in response, but had '%s' in the test data", c.Name, c.Description, desc) + } + delete(capMap, c.Name) + } + } + + for c,_ := range capMap { + t.Errorf("Capability '%s' existed in the test data but didn't appear in the response!", c) + } +} diff --git a/traffic_ops/traffic_ops_golang/capabilities/capabilities.go b/traffic_ops/traffic_ops_golang/capabilities/capabilities.go new file mode 100644 index 0000000000..d59b1c78ce --- /dev/null +++ b/traffic_ops/traffic_ops_golang/capabilities/capabilities.go @@ -0,0 +1,164 @@ +package capabilities + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import "database/sql" +import "encoding/json" +import "errors" +import "fmt" +import "net/http" + +import "github.com/apache/trafficcontrol/lib/go-tc" +import "github.com/apache/trafficcontrol/lib/go-util" + +import "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api" +import "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers" + +const readQuery = ` +SELECT description, + last_updated, + name +FROM capability +` + +const createQuery = ` +INSERT INTO capability (name, description) +VALUES ($1, $2) +RETURNING description, last_updated, name +` + +func Read(w http.ResponseWriter, r *http.Request) { + inf, sysErr, userErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + cols := map[string]dbhelpers.WhereColumnInfo{ + "name": dbhelpers.WhereColumnInfo{"capability.name", nil}, + } + + where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, cols) + if len(errs) > 0 { + errCode = http.StatusBadRequest + userErr = util.JoinErrs(errs) + api.HandleErr(w, r, tx, errCode, userErr, nil) + return + } + + query := readQuery + where + orderBy + pagination + rows, err := inf.Tx.NamedQuery(query, queryValues) + if err != nil && err != sql.ErrNoRows { + errCode = http.StatusInternalServerError + sysErr = fmt.Errorf("querying capabilities: %v", err) + api.HandleErr(w, r, tx, errCode, nil, sysErr) + return + } + defer rows.Close() + + caps := []tc.Capability{} + for rows.Next() { + cap := tc.Capability{} + if err := rows.Scan(&cap.Description, &cap.LastUpdated, &cap.Name); err != nil { + errCode = http.StatusInternalServerError + sysErr = fmt.Errorf("Parsing database response: %v", err) + api.HandleErr(w, r, tx, errCode, nil, sysErr) + return + } + + caps = append(caps, cap) + } + + api.WriteResp(w, r, caps) +} + +func Create(w http.ResponseWriter, r *http.Request) { + inf, sysErr, userErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + decoder := json.NewDecoder(r.Body) + var cap tc.Capability + if err := decoder.Decode(&cap); err != nil { + sysErr = fmt.Errorf("Decoding request body: %v", err) + errCode = http.StatusInternalServerError + api.HandleErr(w, r, tx, errCode, nil, sysErr) + return + } + + if cap.Name == "" { + userErr = errors.New("'name' must be defined! (and not empty)") + errCode = http.StatusBadRequest + api.HandleErr(w, r, tx, errCode, userErr, nil) + return + } + + if cap.Description == "" { + userErr = errors.New("'description' must be defined! (and not empty)") + errCode = http.StatusBadRequest + api.HandleErr(w, r, tx, errCode, userErr, nil) + return + } + + if ok, err := capabilityNameExists(cap.Name, tx); err != nil { + sysErr = fmt.Errorf("Checking for capability %s's existence: %v", cap.Name, err) + errCode = http.StatusInternalServerError + api.HandleErr(w, r, tx, errCode, nil, sysErr) + return + } else if ok { + userErr = fmt.Errorf("Capability '%s' already exists!", cap.Name) + errCode = http.StatusConflict + api.HandleErr(w, r, tx, errCode, userErr, nil) + return + } + + row := tx.QueryRow(createQuery, cap.Name, cap.Description) + if err := row.Scan(&cap.Description, &cap.LastUpdated, &cap.Name); err != nil { + sysErr = fmt.Errorf("Inserting capability: %v", err) + errCode = http.StatusInternalServerError + api.HandleErr(w, r, tx, errCode, nil, sysErr) + return + } + + alerts := tc.CreateAlerts(tc.SuccessLevel, "Capability created.") + alerts.AddNewAlert(tc.WarnLevel, "This endpoint is deprecated, and will be removed in the future") + + api.WriteAlertsObj(w, r, http.StatusOK, alerts, cap) + api.CreateChangeLogRawTx(api.ApiChange, fmt.Sprintf("CAPABILITY: %s, ACTION: Created", cap.Name), inf.User, tx) +} + + +func capabilityNameExists(c string, tx *sql.Tx) (bool, error) { + row := tx.QueryRow(`SELECT name FROM capability WHERE name=$1`, c) + var n string + if err := row.Scan(&n); err != nil { + if err == sql.ErrNoRows { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go index 50f691b4e6..4ef6c79e63 100644 --- a/traffic_ops/traffic_ops_golang/routing/routes.go +++ b/traffic_ops/traffic_ops_golang/routing/routes.go @@ -46,6 +46,7 @@ import ( "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cachegroup" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cachegroupparameter" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cachesstats" + "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/capabilities" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cdn" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cdnfederation" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/coordinate" @@ -168,6 +169,10 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) { {1.1, http.MethodGet, `cachegroups/{id}/unassigned_parameters/?(\.json)?$`, api.ReadHandler(&cachegroupparameter.TOCacheGroupUnassignedParameter{}), auth.PrivLevelReadOnly, Authenticated, nil, 1457339250, perlBypass}, {1.1, http.MethodDelete, `cachegroupparameters/{cachegroupID}/{parameterId}$`, api.DeleteHandler(&cachegroupparameter.TOCacheGroupParameter{}), auth.PrivLevelOperations, Authenticated, nil, 912449733, perlBypass}, + //Capabilities + {1.1, http.MethodGet, `capabilities(/|\.json)?$`, capabilities.Read, auth.PrivLevelReadOnly, Authenticated, nil, 8008135, perlBypass}, + {1.1, http.MethodPost, `capabilities(/|\.json)?$`, capabilities.Create, auth.PrivLevelOperations, Authenticated, nil, -1, perlBypass}, + //CDN {1.1, http.MethodGet, `cdns/name/{name}/sslkeys/?(\.json)?$`, cdn.GetSSLKeys, auth.PrivLevelAdmin, Authenticated, nil, 1278581772, noPerlBypass}, {1.1, http.MethodGet, `cdns/metric_types`, notImplementedHandler, 0, NoAuth, nil, 683165463, noPerlBypass}, // MUST NOT end in $, because the 1.x route is longer diff --git a/traffic_portal/app/src/common/api/CapabilityService.js b/traffic_portal/app/src/common/api/CapabilityService.js index a2440ff5d4..16e34ec66b 100644 --- a/traffic_portal/app/src/common/api/CapabilityService.js +++ b/traffic_portal/app/src/common/api/CapabilityService.js @@ -30,18 +30,6 @@ var CapabilityService = function($http, messageModel, ENV) { ); }; - // todo: change to use query param when it is supported - this.getCapability = function(name) { - return $http.get(ENV.api['root'] + 'capabilities/' + name).then( - function(result) { - return result.data.response[0]; - }, - function(err) { - throw err; - } - ); - }; - this.createCapability = function(cap) { return $http.post(ENV.api['root'] + "capabilities", cap).then( function(result) { @@ -56,7 +44,7 @@ var CapabilityService = function($http, messageModel, ENV) { // todo: change to use query param when it is supported this.updateCapability = function(cap) { - return $http.put(ENV.api['root'] + "capabilities/" + cap.name, cap).then( + return $http.put(ENV.api['root'] + "capabilities", {params: {"name": cap.name}, data: cap}).then( function(result) { return result.data; }, @@ -69,7 +57,7 @@ var CapabilityService = function($http, messageModel, ENV) { // todo: change to use query param when it is supported this.deleteCapability = function(cap) { - return $http.delete(ENV.api['root'] + "capabilities/" + cap.name).then( + return $http.delete(ENV.api['root'] + "capabilities", {params: {"name": cap.name}}).then( function(result) { return result.data; }, diff --git a/traffic_portal/app/src/modules/private/capabilities/edit/index.js b/traffic_portal/app/src/modules/private/capabilities/edit/index.js index b51cd6627c..5f64793599 100644 --- a/traffic_portal/app/src/modules/private/capabilities/edit/index.js +++ b/traffic_portal/app/src/modules/private/capabilities/edit/index.js @@ -28,7 +28,7 @@ module.exports = angular.module('trafficPortal.private.capabilities.edit', []) controller: 'FormEditCapabilityController', resolve: { capability: function($stateParams, capabilityService) { - return capabilityService.getCapability($stateParams.capName); + return capabilityService.getCapabilities({"name": $stateParams.capName}); } } } diff --git a/traffic_portal/app/src/modules/private/capabilities/endpoints/index.js b/traffic_portal/app/src/modules/private/capabilities/endpoints/index.js index 323a9fb110..48758166b3 100644 --- a/traffic_portal/app/src/modules/private/capabilities/endpoints/index.js +++ b/traffic_portal/app/src/modules/private/capabilities/endpoints/index.js @@ -28,7 +28,7 @@ module.exports = angular.module('trafficPortal.private.capabilities.endpoints', controller: 'TableCapabilityEndpointsController', resolve: { capability: function($stateParams, capabilityService) { - return capabilityService.getCapability($stateParams.capName); + return capabilityService.getCapabilities({"name": $stateParams.capName}); }, capEndpoints: function($stateParams, endpointService) { return endpointService.getEndpoints({ capability: $stateParams.capName }); diff --git a/traffic_portal/app/src/modules/private/capabilities/users/index.js b/traffic_portal/app/src/modules/private/capabilities/users/index.js index 42f4d554da..b1aa6ccda4 100644 --- a/traffic_portal/app/src/modules/private/capabilities/users/index.js +++ b/traffic_portal/app/src/modules/private/capabilities/users/index.js @@ -28,7 +28,7 @@ module.exports = angular.module('trafficPortal.private.capabilities.users', []) controller: 'TableCapabilityUsersController', resolve: { capability: function($stateParams, capabilityService) { - return capabilityService.getCapability($stateParams.capName); + return capabilityService.getCapabilities({"name": $stateParams.capName}); }, capUsers: function($stateParams, userService) { return userService.getUsers({ capability: $stateParams.capName });