Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,13 @@ Key design choices include:
}
```

The endpoint returns `403 Forbidden` if the unit is not inside one of the unit groups of the
requesting user.

The username of the requesting user is sent to the unit, which records it in its own logs
along with the account used by the controller. Units older than `ns-api-server` 1.6.0 ignore
it.

- `POST /units`

REQ
Expand Down
15 changes: 15 additions & 0 deletions api/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,21 @@ func TestUnitAuthorization(t *testing.T) {
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code, "limited user should not be able to get unitId_2")

// Test GET /units/<unit_id_2>/token with limited user - should return 403 Forbidden
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/units/"+unitId_2+"/token", nil)
req.Header.Set("Authorization", "Bearer "+limitedToken)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code, "limited user should not be able to get a token for unitId_2")

// Test GET /units/<unit_id_1>/token with limited user - the unit is
// unreachable in tests, so the request fails after the access check
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/units/"+unitId_1+"/token", nil)
req.Header.Set("Authorization", "Bearer "+limitedToken)
router.ServeHTTP(w, req)
assert.NotEqual(t, http.StatusForbidden, w.Code, "limited user should be allowed to get a token for unitId_1")

// Test GET /units with limited user - should only return unitId_1
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/units", nil)
Expand Down
26 changes: 19 additions & 7 deletions api/methods/unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,17 @@ func GetUnit(c *gin.Context) {
func GetToken(c *gin.Context) {
// get unit id
unitId := c.Param("unit_id")
user := jwt.ExtractClaims(c)["id"].(string)
if !UserCanAccessUnit(user, unitId) {
c.JSON(http.StatusForbidden, structs.Map(response.StatusForbidden{
Code: 403,
Message: "user does not have access to this unit",
Data: nil,
}))
return
}

token, expire, err := getUnitToken(unitId)
token, expire, err := getUnitToken(unitId, user)

if err != nil {
c.JSON(http.StatusBadRequest, structs.Map(response.StatusBadRequest{
Expand Down Expand Up @@ -142,7 +151,7 @@ func GetUnitInfo(c *gin.Context) {
}

// get unit info and store it
info, err := GetRemoteInfo(unitId)
info, err := GetRemoteInfo(unitId, user)

// check errors
if err != nil {
Expand Down Expand Up @@ -554,7 +563,7 @@ func ListConnectedUnits() ([]string, error) {
return storage.ListConnectedUnits()
}

func getUnitToken(unitId string) (string, string, error) {
func getUnitToken(unitId string, onBehalfOf string) (string, string, error) {

// read credentials
username, password, err := storage.GetUnitCredentials(unitId)
Expand All @@ -567,8 +576,9 @@ func getUnitToken(unitId string) (string, string, error) {

// create request action
credentials := models.LoginRequest{
Username: username,
Password: password,
Username: username,
Password: password,
OnBehalfOf: onBehalfOf,
}
body, err := json.Marshal(credentials)
if err != nil {
Expand Down Expand Up @@ -607,9 +617,11 @@ func getUnitToken(unitId string) (string, string, error) {
return loginResponse.Token, loginResponse.Expire, nil
}

func GetRemoteInfo(unitId string) (models.UnitInfo, error) {
// GetRemoteInfo takes an empty onBehalfOf when called by a routine, so that the
// unit logs the request as the controller itself
func GetRemoteInfo(unitId string, onBehalfOf string) (models.UnitInfo, error) {
// get the unit token and execute the request
token, _, _ := getUnitToken(unitId)
token, _, _ := getUnitToken(unitId, onBehalfOf)
if token == "" {
return models.UnitInfo{}, errors.New("error getting token")
}
Expand Down
2 changes: 2 additions & 0 deletions api/models/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ package models
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
// controller user originating the request, ignored by older units
OnBehalfOf string `json:"on_behalf_of,omitempty"`
}

type LoginResponse struct {
Expand Down
3 changes: 2 additions & 1 deletion api/routines/routines.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ func RefreshRemoteInfoLoop() {
}

for _, unit := range units {
_, err := methods.GetRemoteInfo(unit)
// no user originated this request
_, err := methods.GetRemoteInfo(unit, "")
if err != nil {
logs.Logs.Println("[ERR][ROUTINE] loop for remote info failed: " + err.Error())
}
Expand Down
Loading