Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add support for rack access #3664

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Expand Up @@ -46,6 +46,8 @@ require (

require (
github.com/adrg/xdg v0.2.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
3 changes: 3 additions & 0 deletions go.sum
Expand Up @@ -272,6 +272,8 @@ github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
Expand Down Expand Up @@ -438,6 +440,7 @@ github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.1.0 h1:g0fH8RicVgNl+zVZDCDfbdWxAWoAEJyI7I3TZYXFiig=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 h1:eajwn6K3weW5cd1ZXLu2sJ4pvwlBiCWY4uDejOr73gM=
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
Expand Down
24 changes: 22 additions & 2 deletions pkg/api/api.go
@@ -1,8 +1,10 @@
package api

import (
"net/http"
"reflect"

"github.com/convox/rack/pkg/jwt"
"github.com/convox/rack/pkg/structs"
"github.com/convox/rack/provider"
"github.com/convox/stdapi"
Expand All @@ -12,6 +14,7 @@
*stdapi.Server
Password string
Provider structs.Provider
JwtMngr *jwt.JwtManager
}

func New() (*Server, error) {
Expand All @@ -28,9 +31,15 @@
panic(err)
}

key, err := p.SystemJwtSignKey()
if err != nil {
panic(err)

Check warning on line 36 in pkg/api/api.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/api.go#L36

Added line #L36 was not covered by tests
}

s := &Server{
Provider: p,
Server: stdapi.New("api", "api"),
JwtMngr: jwt.NewJwtManager(key),
}

s.Server.Router.Router = s.Server.Router.Router.SkipClean(true)
Expand All @@ -54,9 +63,20 @@

func (s *Server) authenticate(next stdapi.HandlerFunc) stdapi.HandlerFunc {
return func(c *stdapi.Context) error {
if _, pass, _ := c.Request().BasicAuth(); s.Password != "" && s.Password != pass {
return stdapi.Errorf(401, "invalid authentication")
username, pass, _ := c.Request().BasicAuth()
if username == "jwt" && s.JwtMngr != nil {
data, err := s.JwtMngr.Verify(pass)
if err != nil {
return stdapi.Errorf(http.StatusUnauthorized, "invalid authentication: %s", err)

Check warning on line 70 in pkg/api/api.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/api.go#L68-L70

Added lines #L68 - L70 were not covered by tests
}
c.Set(structs.ConvoxRoleParam, data.Role)

Check warning on line 72 in pkg/api/api.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/api.go#L72

Added line #L72 was not covered by tests
} else {
if s.Password != "" && s.Password != pass {
return stdapi.Errorf(http.StatusUnauthorized, "invalid authentication")
}
SetReadWriteRole(c)
}

return next(c)
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/api/api_test.go
Expand Up @@ -19,6 +19,7 @@ func testServer(t *testing.T, fn func(*stdsdk.Client, *structs.MockProvider)) {
p := &structs.MockProvider{}
p.On("Initialize", mock.Anything).Return(nil)
p.On("WithContext", mock.Anything).Return(p).Maybe()
p.On("SystemJwtSignKey").Return("", nil)

s := api.NewWithProvider(p)
s.Logger = logger.Discard
Expand Down
1 change: 1 addition & 0 deletions pkg/api/auth_test.go
Expand Up @@ -18,6 +18,7 @@ import (
func TestAuthentication(t *testing.T) {
p := &structs.MockProvider{}
p.On("Initialize", mock.Anything).Return(nil)
p.On("SystemJwtSignKey").Return("", nil)

s := api.NewWithProvider(p)
s.Logger = logger.Discard
Expand Down
49 changes: 49 additions & 0 deletions pkg/api/authorization.go
@@ -0,0 +1,49 @@
package api

import (
"net/http"
"strings"

"github.com/convox/rack/pkg/structs"
"github.com/convox/stdapi"
)

func (s *Server) Authorize(next stdapi.HandlerFunc) stdapi.HandlerFunc {
return func(c *stdapi.Context) error {
switch c.Request().Method {
case http.MethodGet:
if !CanRead(c) {
return stdapi.Errorf(401, "you are unauthorized to access this")
nightfury1204 marked this conversation as resolved.
Show resolved Hide resolved
}
default:
if !CanWrite(c) {
return stdapi.Errorf(401, "you are unauthorized to access this")
}
}
return next(c)
}
}

func CanRead(c *stdapi.Context) bool {
if d := c.Get(structs.ConvoxRoleParam); d != nil {
v, _ := d.(string)
return strings.Contains(v, "r")
}
return false
}

func CanWrite(c *stdapi.Context) bool {
if d := c.Get(structs.ConvoxRoleParam); d != nil {
v, _ := d.(string)
return strings.Contains(v, "w")
}
return false

Check warning on line 40 in pkg/api/authorization.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/authorization.go#L40

Added line #L40 was not covered by tests
}

func SetReadRole(c *stdapi.Context) {
c.Set(structs.ConvoxRoleParam, structs.ConvoxRoleRead)
}

func SetReadWriteRole(c *stdapi.Context) {
c.Set(structs.ConvoxRoleParam, structs.ConvoxRoleReadWrite)
}
63 changes: 63 additions & 0 deletions pkg/api/authorization_test.go
@@ -0,0 +1,63 @@
package api_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/convox/rack/pkg/api"
"github.com/convox/stdapi"
"github.com/stretchr/testify/assert"
)

func TestAuthorize(t *testing.T) {
s := &api.Server{}

testData := []struct {
c *stdapi.Context
access bool
}{
{
c: func() *stdapi.Context {
c := stdapi.NewContext(nil, httptest.NewRequest(http.MethodGet, "http://text.com", nil))
api.SetReadRole(c)
return c
}(),
access: true,
},
{
c: func() *stdapi.Context {
c := stdapi.NewContext(nil, httptest.NewRequest(http.MethodGet, "http://text.com", nil))
return c
}(),
access: false,
},
{
c: func() *stdapi.Context {
c := stdapi.NewContext(nil, httptest.NewRequest(http.MethodPost, "http://text.com", nil))
api.SetReadRole(c)
return c
}(),
access: false,
},
{
c: func() *stdapi.Context {
c := stdapi.NewContext(nil, httptest.NewRequest(http.MethodPost, "http://text.com", nil))
api.SetReadWriteRole(c)
return c
}(),
access: true,
},
}

for _, td := range testData {
err := s.Authorize(func(c *stdapi.Context) error {
return nil
})(td.c)
if td.access {
assert.Nil(t, err)
} else {
assert.NotNil(t, err)
}
}
}
36 changes: 36 additions & 0 deletions pkg/api/controllers.go
Expand Up @@ -5,6 +5,7 @@
"sort"
"strconv"
"strings"
"time"

"github.com/convox/rack/pkg/structs"
"github.com/convox/stdapi"
Expand Down Expand Up @@ -1176,6 +1177,41 @@
return stdapi.Errorf(404, "not available via api")
}

func (s *Server) SystemJwtSignKeyRotate(c *stdapi.Context) error {
_, err := s.provider(c).WithContext(c.Context()).SystemJwtSignKeyRotate()
if err != nil {
return err

Check warning on line 1183 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1180-L1183

Added lines #L1180 - L1183 were not covered by tests
}
return c.RenderOK()

Check warning on line 1185 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1185

Added line #L1185 was not covered by tests
}

func (s *Server) SystemJwtToken(c *stdapi.Context) error {
role := c.Value("role")
durationInHour, err := strconv.Atoi(c.Value("durationInHour"))
if err != nil {
return stdapi.Errorf(404, "invalid duration")

Check warning on line 1192 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1188-L1192

Added lines #L1188 - L1192 were not covered by tests
}

var tk string

Check warning on line 1195 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1195

Added line #L1195 was not covered by tests

switch role {
case "read":
tk, err = s.JwtMngr.ReadToken(time.Hour * time.Duration(durationInHour))
if err != nil {
return err

Check warning on line 1201 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1197-L1201

Added lines #L1197 - L1201 were not covered by tests
}
case "write":
tk, err = s.JwtMngr.WriteToken(time.Hour * time.Duration(durationInHour))
if err != nil {
return err

Check warning on line 1206 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1203-L1206

Added lines #L1203 - L1206 were not covered by tests
}
}

return c.RenderJSON(map[string]string{
"token": tk,
})

Check warning on line 1212 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1210-L1212

Added lines #L1210 - L1212 were not covered by tests
}

func (s *Server) SystemLogs(c *stdapi.Context) error {
if err := s.hook("SystemLogsValidate", c); err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions pkg/api/routes.go
Expand Up @@ -3,6 +3,8 @@ package api
import "github.com/convox/stdapi"

func (s *Server) setupRoutes(r stdapi.Router) {
r.Use(s.Authorize)

r.Route("POST", "/apps/{name}/cancel", s.AppCancel)
r.Route("POST", "/apps", s.AppCreate)
r.Route("DELETE", "/apps/{name}", s.AppDelete)
Expand Down Expand Up @@ -60,6 +62,8 @@ func (s *Server) setupRoutes(r stdapi.Router) {
r.Route("PUT", "/apps/{app}/services/{name}", s.ServiceUpdate)
r.Route("GET", "/system", s.SystemGet)
r.Route("", "", s.SystemInstall)
r.Route("PUT", "/system/jwt/rotate", s.SystemJwtSignKeyRotate)
r.Route("POST", "/system/jwt/token", s.SystemJwtToken)
r.Route("SOCKET", "/system/logs", s.SystemLogs)
r.Route("GET", "/system/metrics", s.SystemMetrics)
r.Route("GET", "/system/processes", s.SystemProcesses)
Expand Down
52 changes: 52 additions & 0 deletions pkg/cli/rack.go
Expand Up @@ -7,6 +7,7 @@
"net/url"
"os"
"sort"
"strconv"
"strings"

"github.com/aws/aws-sdk-go/aws"
Expand All @@ -27,6 +28,20 @@
Validate: stdcli.Args(0),
})

register("rack access", "get rack access creds", RackAccess, stdcli.CommandOptions{
Flags: []stdcli.Flag{
flagRack,
stdcli.StringFlag("role", "", "access role: read or write"),
stdcli.IntFlag("duration-in-hour", "", "duration in hours"),
},
Validate: stdcli.Args(0),
})

register("rack access key rotate", "rotate access key", RackAccessKeyRotate, stdcli.CommandOptions{
Flags: []stdcli.Flag{flagRack},
Validate: stdcli.Args(0),
})

registerWithoutProvider("rack install", "install a rack", RackInstall, stdcli.CommandOptions{
Flags: append(stdcli.OptionFlags(structs.SystemInstallOptions{})),
Usage: "<type> [Parameter=Value]...",
Expand Down Expand Up @@ -128,6 +143,43 @@
return i.Print()
}

func RackAccess(rack sdk.Interface, c *stdcli.Context) error {
rData, err := rack.SystemGet()
if err != nil {
return err

Check warning on line 149 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L146-L149

Added lines #L146 - L149 were not covered by tests
}

role, ok := c.Value("role").(string)
if !ok {
return fmt.Errorf("role is required")

Check warning on line 154 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L152-L154

Added lines #L152 - L154 were not covered by tests
}

duration, ok := c.Value("duration-in-hour").(int)
if !ok {
return fmt.Errorf("duration is required")

Check warning on line 159 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L157-L159

Added lines #L157 - L159 were not covered by tests
}

jwtTk, err := rack.SystemJwtToken(structs.SystemJwtOptions{
Role: options.String(role),
DurationInHour: options.String(strconv.Itoa(duration)),
})
if err != nil {
fmt.Println(err)
return err

Check warning on line 168 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L162-L168

Added lines #L162 - L168 were not covered by tests
}

return c.Writef("RACK_URL=https://jwt:%s@%s\n", jwtTk.Token, rData.RackDomain)

Check warning on line 171 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L171

Added line #L171 was not covered by tests
}

func RackAccessKeyRotate(rack sdk.Interface, c *stdcli.Context) error {
_, err := rack.SystemJwtSignKeyRotate()
if err != nil {
return err

Check warning on line 177 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L174-L177

Added lines #L174 - L177 were not covered by tests
}

return c.OK()

Check warning on line 180 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L180

Added line #L180 was not covered by tests
}

func RackInstall(rack sdk.Interface, c *stdcli.Context) error {
var opts structs.SystemInstallOptions

Expand Down