Skip to content

Commit

Permalink
Merge pull request #6 from uselagoon/metrics
Browse files Browse the repository at this point in the history
Add metrics and update project identification logic
  • Loading branch information
smlx authored Jan 25, 2022
2 parents fc98bcc + d15b2ef commit 5519324
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 44 deletions.
12 changes: 10 additions & 2 deletions cmd/service-api/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import (
"github.com/go-sql-driver/mysql"
"github.com/uselagoon/ssh-portal/internal/keycloak"
"github.com/uselagoon/ssh-portal/internal/lagoondb"
"github.com/uselagoon/ssh-portal/internal/serviceapi"
"github.com/uselagoon/ssh-portal/internal/metrics"
"github.com/uselagoon/ssh-portal/internal/server"
"go.uber.org/zap"
)

Expand Down Expand Up @@ -46,6 +47,13 @@ func getContext() (context.Context, func()) {

// Run the serve command to service API requests.
func (cmd *ServeCmd) Run(log *zap.Logger) error {
// instrumentation requires a separate context because deferred Shutdown()
// will exit immediately if the context is already done.
ictx := context.Background()
// init metrics
m := metrics.NewServer(log)
defer m.Shutdown(ictx) //nolint:errcheck
// get main process context
ctx, cancel := getContext()
defer cancel()
// init lagoon DB client
Expand All @@ -66,5 +74,5 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error {
return fmt.Errorf("couldn't init keycloak Client: %v", err)
}
// start serving NATS requests
return serviceapi.ServeNATS(ctx, log, l, k, cmd.NATSServer)
return server.ServeNATS(ctx, log, l, k, cmd.NATSServer)
}
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,33 @@ require (
github.com/google/uuid v1.3.0
github.com/jmoiron/sqlx v1.3.4
github.com/nats-io/nats.go v1.13.1-0.20211018182449-f2416a8b1483
github.com/prometheus/client_golang v1.11.0
go.opentelemetry.io/otel v1.3.0
go.uber.org/zap v1.19.1
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
gopkg.in/square/go-jose.v2 v2.6.0
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/go-logr/logr v1.2.1 // indirect
github.com/go-logr/stdr v1.2.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/nats-io/nats-server/v2 v2.6.4 // indirect
github.com/nats-io/nkeys v0.3.0 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
go.opentelemetry.io/otel/trace v1.3.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)
95 changes: 93 additions & 2 deletions go.sum

Large diffs are not rendered by default.

24 changes: 16 additions & 8 deletions internal/keycloak/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ import (
"time"

"github.com/google/uuid"
"go.opentelemetry.io/otel"
"go.uber.org/zap"
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2/jwt"
)

const pkgName = "github.com/uselagoon/ssh-portal/internal/keycloak"

// Client is a keycloak client.
type Client struct {
ctx context.Context
baseURL *url.URL
clientID string
clientSecret string
Expand All @@ -36,12 +38,11 @@ func NewClient(ctx context.Context, log *zap.Logger, baseURL, clientID,
if err != nil {
return nil, fmt.Errorf("couldn't parse base URL %s: %v", baseURL, err)
}
pubKey, err := publicKey(*u)
pubKey, err := publicKey(ctx, *u)
if err != nil {
return nil, fmt.Errorf("couldn't get realm public key: %v", err)
}
return &Client{
ctx: ctx,
baseURL: u,
clientID: clientID,
clientSecret: clientSecret,
Expand All @@ -52,11 +53,15 @@ func NewClient(ctx context.Context, log *zap.Logger, baseURL, clientID,

// publicKey queries the keycloak lagoon realm metadata endpoint and returns
// the RSA public key used to sign JWTs
func publicKey(u url.URL) (*rsa.PublicKey, error) {
func publicKey(ctx context.Context, u url.URL) (*rsa.PublicKey, error) {
// get the metadata JSON
client := &http.Client{Timeout: 10 * time.Second}
u.Path = path.Join(u.Path, `/auth/realms/lagoon`)
res, err := client.Get(u.String())
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return nil, fmt.Errorf("couldn't construct request: %v", err)
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("couldn't get realm metadata: %v", err)
}
Expand Down Expand Up @@ -96,8 +101,11 @@ func publicKey(u url.URL) (*rsa.PublicKey, error) {
// UserRolesAndGroups queries Keycloak given the user UUID, and returns the
// user's realm roles, group memberships, and the project IDs associated with
// those groups.
func (c *Client) UserRolesAndGroups(userUUID *uuid.UUID) ([]string, []string,
map[string][]int, error) {
func (c *Client) UserRolesAndGroups(ctx context.Context,
userUUID *uuid.UUID) ([]string, []string, map[string][]int, error) {
// set up tracing
ctx, span := otel.Tracer(pkgName).Start(ctx, "UserRolesAndGroups")
defer span.End()
// get user token
tokenURL := *c.baseURL
tokenURL.Path = path.Join(tokenURL.Path,
Expand All @@ -109,7 +117,7 @@ func (c *Client) UserRolesAndGroups(userUUID *uuid.UUID) ([]string, []string,
TokenURL: tokenURL.String(),
},
}
ctx := context.WithValue(c.ctx, oauth2.HTTPClient, &http.Client{
ctx = context.WithValue(ctx, oauth2.HTTPClient, &http.Client{
Timeout: 10 * time.Second,
})
userToken, err := userConfig.Exchange(ctx, "",
Expand Down
33 changes: 23 additions & 10 deletions internal/lagoondb/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,27 @@ import (
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/uselagoon/ssh-portal/internal/lagoon"
"go.opentelemetry.io/otel"
)

const pkgName = "github.com/uselagoon/ssh-portal/internal/lagoondb"

// SSHAccessQuery defines the structure of an SSH access query.
type SSHAccessQuery struct {
SSHFingerprint string
NamespaceName string
ProjectID int
EnvironmentID int
}

// Client is a Lagoon API-DB client
type Client struct {
db *sqlx.DB
ctx context.Context
db *sqlx.DB
}

// Environment is a Lagoon project environment.
type Environment struct {
ID int `db:"id"`
Name string `db:"name"`
NamespaceName string `db:"namespace_name"`
ProjectID int `db:"project_id"`
Expand All @@ -51,22 +56,26 @@ func NewClient(ctx context.Context, dsn string) (*Client, error) {
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
return &Client{
db: db,
ctx: ctx,
db: db,
}, nil
}

// EnvironmentByNamespaceName returns the Environment associated with the given
// Namespace name (on Openshift this is the project name).
func (c *Client) EnvironmentByNamespaceName(name string) (*Environment, error) {
func (c *Client) EnvironmentByNamespaceName(ctx context.Context, name string) (*Environment, error) {
// set up tracing
ctx, span := otel.Tracer(pkgName).Start(ctx, "EnvironmentByNamespaceName")
defer span.End()
// run query
env := Environment{}
err := c.db.GetContext(c.ctx, &env, `
err := c.db.GetContext(ctx, &env, `
SELECT
environment.environment_type AS type,
environment.id AS id,
environment.name AS name,
environment.openshift_project_name AS namespace_name,
project.id AS project_id,
project.name AS project_name,
environment.environment_type AS type
project.name AS project_name
FROM environment JOIN project ON environment.project = project.id
WHERE environment.openshift_project_name = ?`, name)
if err != nil {
Expand All @@ -80,9 +89,13 @@ func (c *Client) EnvironmentByNamespaceName(name string) (*Environment, error) {

// UserBySSHFingerprint returns the User associated with the given
// SSH fingerprint.
func (c *Client) UserBySSHFingerprint(fingerprint string) (*User, error) {
func (c *Client) UserBySSHFingerprint(ctx context.Context, fingerprint string) (*User, error) {
// set up tracing
ctx, span := otel.Tracer(pkgName).Start(ctx, "UserBySSHFingerprint")
defer span.End()
// run query
user := User{}
err := c.db.GetContext(c.ctx, &user, `
err := c.db.GetContext(ctx, &user, `
SELECT user_ssh_key.usid AS uuid
FROM user_ssh_key JOIN ssh_key ON user_ssh_key.skid = ssh_key.id
WHERE ssh_key.key_fingerprint = ?`, fingerprint)
Expand Down
29 changes: 29 additions & 0 deletions internal/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package metrics

import (
"net/http"
"time"

"github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
)

// NewServer returns a *http.Server serving prometheus metrics in a new
// goroutine.
// Caller should defer Shutdown() for cleanup.
func NewServer(log *zap.Logger) *http.Server {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
s := http.Server{
Addr: ":9911",
Handler: mux,
ReadTimeout: 16 * time.Second,
WriteTimeout: 16 * time.Second,
}
go func() {
if err := s.ListenAndServe(); err != http.ErrServerClosed {
log.Error("metrics server did not shut down cleanly", zap.Error(err))
}
}()
return &s
}
11 changes: 9 additions & 2 deletions internal/permission/usercansshtoenvironment.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package permission

import (
"context"
"fmt"

"github.com/uselagoon/ssh-portal/internal/lagoon"
"github.com/uselagoon/ssh-portal/internal/lagoondb"
"go.opentelemetry.io/otel"
)

const pkgName = "github.com/uselagoon/ssh-portal/internal/permission"

// map environment type to role which can SSH
var envTypeRoleCanSSH = map[lagoon.EnvironmentType][]lagoon.UserRole{
lagoon.Development: {
Expand All @@ -23,8 +27,11 @@ var envTypeRoleCanSSH = map[lagoon.EnvironmentType][]lagoon.UserRole{
// UserCanSSHToEnvironment returns true if the given environment can be
// connected to via SSH by the user with the given realm roles and user groups,
// and false otherwise.
func UserCanSSHToEnvironment(env *lagoondb.Environment, realmRoles,
userGroups []string, groupProjectIDs map[string][]int) bool {
func UserCanSSHToEnvironment(ctx context.Context, env *lagoondb.Environment,
realmRoles, userGroups []string, groupProjectIDs map[string][]int) bool {
// set up tracing
_, span := otel.Tracer(pkgName).Start(ctx, "UserCanSSHToEnvironment")
defer span.End()
// check for platform owner
for _, r := range realmRoles {
if r == "platform-owner" {
Expand Down
6 changes: 4 additions & 2 deletions internal/permission/usercansshtoenvironment_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package permission_test

import (
"context"
"testing"

"github.com/uselagoon/ssh-portal/internal/lagoon"
Expand Down Expand Up @@ -173,8 +174,9 @@ func TestUserCanSSH(t *testing.T) {
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
response := permission.UserCanSSHToEnvironment(tc.input.env,
tc.input.realmRoles, tc.input.userGroups, tc.input.groupProjectIDs)
response := permission.UserCanSSHToEnvironment(context.Background(),
tc.input.env, tc.input.realmRoles, tc.input.userGroups,
tc.input.groupProjectIDs)
if response != tc.expect {
tt.Fatalf("expected %v, got %v", tc.expect, response)
}
Expand Down
Loading

0 comments on commit 5519324

Please sign in to comment.