Merge pull request #1422 from jim-minter/dbtoken

DB token service for gateway
This commit is contained in:
Mangirdas Judeikis 2021-04-27 15:49:44 +01:00 коммит произвёл GitHub
Родитель 2a3256f2a8 3b4359927c
Коммит a3099f14ba
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
57 изменённых файлов: 2096 добавлений и 153 удалений

Просмотреть файл

@ -2,5 +2,5 @@ FROM registry.access.redhat.com/ubi8/ubi-minimal
RUN microdnf update && microdnf clean all
COPY aro e2e.test /usr/local/bin/
ENTRYPOINT ["aro"]
EXPOSE 2222/tcp 8443/tcp 8444/tcp
EXPOSE 2222/tcp 8443/tcp 8444/tcp 8445/tcp
USER 1000

Просмотреть файл

@ -17,5 +17,5 @@ FROM registry.access.redhat.com/ubi7/ubi-minimal
RUN microdnf update && microdnf clean all
COPY --from=builder /go/src/github.com/Azure/ARO-RP/aro /go/src/github.com/Azure/ARO-RP/e2e.test /usr/local/bin/
ENTRYPOINT ["aro"]
EXPOSE 2222/tcp 8443/tcp 8444/tcp
EXPOSE 2222/tcp 8443/tcp 8444/tcp 8445/tcp
USER 1000

104
cmd/aro/dbtoken.go Normal file
Просмотреть файл

@ -0,0 +1,104 @@
package main
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"fmt"
"net"
"os"
"github.com/sirupsen/logrus"
"github.com/Azure/ARO-RP/pkg/database"
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
pkgdbtoken "github.com/Azure/ARO-RP/pkg/dbtoken"
"github.com/Azure/ARO-RP/pkg/env"
"github.com/Azure/ARO-RP/pkg/metrics/statsd"
"github.com/Azure/ARO-RP/pkg/util/keyvault"
"github.com/Azure/ARO-RP/pkg/util/oidc"
)
func dbtoken(ctx context.Context, log *logrus.Entry) error {
_env, err := env.NewCore(ctx, log)
if err != nil {
return err
}
if !_env.IsLocalDevelopmentMode() {
for _, key := range []string{
"MDM_ACCOUNT",
"MDM_NAMESPACE",
} {
if _, found := os.LookupEnv(key); !found {
return fmt.Errorf("environment variable %q unset", key)
}
}
}
rpKVAuthorizer, err := _env.NewRPAuthorizer(_env.Environment().ResourceIdentifiers.KeyVault)
if err != nil {
return err
}
m := statsd.New(ctx, log.WithField("component", "dbtoken"), _env, os.Getenv("MDM_ACCOUNT"), os.Getenv("MDM_NAMESPACE"))
dbAuthorizer, err := database.NewMasterKeyAuthorizer(ctx, _env)
if err != nil {
return err
}
dbc, err := database.NewDatabaseClient(log.WithField("component", "database"), _env, dbAuthorizer, m, nil)
if err != nil {
return err
}
dbid, err := database.Name(_env.IsLocalDevelopmentMode())
if err != nil {
return err
}
userc := cosmosdb.NewUserClient(dbc, dbid)
err = pkgdbtoken.ConfigurePermissions(ctx, dbid, userc)
if err != nil {
return err
}
dbtokenKeyvaultURI, err := keyvault.URI(_env, env.DBTokenKeyvaultSuffix)
if err != nil {
return err
}
dbtokenKeyvault := keyvault.NewManager(rpKVAuthorizer, dbtokenKeyvaultURI)
servingKey, servingCerts, err := dbtokenKeyvault.GetCertificateSecret(ctx, env.DBTokenServerSecretName)
if err != nil {
return err
}
verifier, err := oidc.NewVerifier(ctx, "https://sts.windows.net/"+_env.TenantID()+"/", pkgdbtoken.Resource)
if err != nil {
return err
}
address := "localhost:8445"
if !_env.IsLocalDevelopmentMode() {
address = ":8445"
}
l, err := net.Listen("tcp", address)
if err != nil {
return err
}
log.Print("listening")
server, err := pkgdbtoken.NewServer(ctx, _env, log.WithField("component", "dbtoken"), log.WithField("component", "dbtoken-access"), l, servingKey, servingCerts, verifier, userc)
if err != nil {
return err
}
return server.Run(ctx)
}

Просмотреть файл

@ -21,6 +21,7 @@ import (
func usage() {
fmt.Fprint(flag.CommandLine.Output(), "usage:\n")
fmt.Fprintf(flag.CommandLine.Output(), " %s dbtoken\n", os.Args[0])
fmt.Fprintf(flag.CommandLine.Output(), " %s deploy config.yaml location\n", os.Args[0])
fmt.Fprintf(flag.CommandLine.Output(), " %s mirror [release_image...]\n", os.Args[0])
fmt.Fprintf(flag.CommandLine.Output(), " %s monitor\n", os.Args[0])
@ -48,6 +49,9 @@ func main() {
var err error
switch strings.ToLower(flag.Arg(0)) {
case "dbtoken":
checkArgs(1)
err = dbtoken(ctx, log)
case "deploy":
checkArgs(3)
err = deploy(ctx, log)

Просмотреть файл

@ -76,7 +76,12 @@ func monitor(ctx context.Context, log *logrus.Entry) error {
return err
}
dbc, err := database.NewDatabaseClient(ctx, log.WithField("component", "database"), _env, &noop.Noop{}, aead)
dbAuthorizer, err := database.NewMasterKeyAuthorizer(ctx, _env)
if err != nil {
return err
}
dbc, err := database.NewDatabaseClient(log.WithField("component", "database"), _env, dbAuthorizer, &noop.Noop{}, aead)
if err != nil {
return err
}

Просмотреть файл

@ -18,10 +18,10 @@ import (
"github.com/Azure/ARO-RP/pkg/env"
"github.com/Azure/ARO-RP/pkg/metrics/statsd"
pkgportal "github.com/Azure/ARO-RP/pkg/portal"
"github.com/Azure/ARO-RP/pkg/portal/middleware"
"github.com/Azure/ARO-RP/pkg/proxy"
"github.com/Azure/ARO-RP/pkg/util/encryption"
"github.com/Azure/ARO-RP/pkg/util/keyvault"
"github.com/Azure/ARO-RP/pkg/util/oidc"
)
func portal(ctx context.Context, log *logrus.Entry, audit *logrus.Entry) error {
@ -87,7 +87,12 @@ func portal(ctx context.Context, log *logrus.Entry, audit *logrus.Entry) error {
return err
}
dbc, err := database.NewDatabaseClient(ctx, log.WithField("component", "database"), _env, m, aead)
dbAuthorizer, err := database.NewMasterKeyAuthorizer(ctx, _env)
if err != nil {
return err
}
dbc, err := database.NewDatabaseClient(log.WithField("component", "database"), _env, dbAuthorizer, m, aead)
if err != nil {
return err
}
@ -140,7 +145,7 @@ func portal(ctx context.Context, log *logrus.Entry, audit *logrus.Entry) error {
}
clientID := os.Getenv("AZURE_PORTAL_CLIENT_ID")
verifier, err := middleware.NewVerifier(ctx, _env, clientID)
verifier, err := oidc.NewVerifier(ctx, _env.Environment().ActiveDirectoryEndpoint+_env.TenantID()+"/v2.0", clientID)
if err != nil {
return err
}

Просмотреть файл

@ -83,7 +83,12 @@ func rp(ctx context.Context, log, audit *logrus.Entry) error {
return err
}
dbc, err := database.NewDatabaseClient(ctx, log.WithField("component", "database"), _env, m, aead)
dbAuthorizer, err := database.NewMasterKeyAuthorizer(ctx, _env)
if err != nil {
return err
}
dbc, err := database.NewDatabaseClient(log.WithField("component", "database"), _env, dbAuthorizer, m, aead)
if err != nil {
return err
}

Просмотреть файл

@ -116,6 +116,47 @@
"location": "[resourceGroup().location]",
"apiVersion": "2016-10-01"
},
{
"properties": {
"tenantId": "[subscription().tenantId]",
"sku": {
"family": "A",
"name": "standard"
},
"accessPolicies": [
{
"tenantId": "[subscription().tenantId]",
"objectId": "[parameters('rpServicePrincipalId')]",
"permissions": {
"secrets": [
"get"
]
}
},
{
"tenantId": "[subscription().tenantId]",
"objectId": "[parameters('adminObjectId')]",
"permissions": {
"secrets": [
"set",
"list"
],
"certificates": [
"delete",
"get",
"import",
"list"
]
}
}
],
"enableSoftDelete": true
},
"name": "[concat(parameters('keyvaultPrefix'), '-dbt')]",
"type": "Microsoft.KeyVault/vaults",
"location": "[resourceGroup().location]",
"apiVersion": "2016-10-01"
},
{
"properties": {
"tenantId": "[subscription().tenantId]",

Просмотреть файл

@ -8,6 +8,9 @@
"extraClusterKeyvaultAccessPolicies": {
"value": []
},
"extraDBTokenKeyvaultAccessPolicies": {
"value": []
},
"extraPortalKeyvaultAccessPolicies": {
"value": []
},

Просмотреть файл

@ -34,6 +34,17 @@
}
}
],
"dbTokenKeyvaultAccessPolicies": [
{
"tenantId": "[subscription().tenantId]",
"objectId": "[parameters('rpServicePrincipalId')]",
"permissions": {
"secrets": [
"get"
]
}
}
],
"portalKeyvaultAccessPolicies": [
{
"tenantId": "[subscription().tenantId]",
@ -66,6 +77,10 @@
"type": "array",
"defaultValue": []
},
"extraDBTokenKeyvaultAccessPolicies": {
"type": "array",
"defaultValue": []
},
"extraPortalKeyvaultAccessPolicies": {
"type": "array",
"defaultValue": []
@ -150,6 +165,21 @@
"location": "[resourceGroup().location]",
"apiVersion": "2016-10-01"
},
{
"properties": {
"tenantId": "[subscription().tenantId]",
"sku": {
"family": "A",
"name": "standard"
},
"accessPolicies": "[concat(variables('dbTokenKeyvaultAccessPolicies'), parameters('extraDBTokenKeyvaultAccessPolicies'))]",
"enableSoftDelete": true
},
"name": "[concat(parameters('keyvaultPrefix'), '-dbt')]",
"type": "Microsoft.KeyVault/vaults",
"location": "[resourceGroup().location]",
"apiVersion": "2016-10-01"
},
{
"properties": {
"tenantId": "[subscription().tenantId]",

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

56
docs/dbtoken-service.md Normal file
Просмотреть файл

@ -0,0 +1,56 @@
# DB token service
## Introduction
Cosmos DB access control is described
[https://docs.microsoft.com/en-us/azure/cosmos-db/secure-access-to-data](here).
In brief, there are three options:
1. use r/w or r/o primary keys, which grant access to the whole database account
2. implement a service which transforms (1) into scoped resource tokens
3. a third AAD RBAC-based model is in preview.
Currently, the RP, monitoring and portal service share the same security
boundary (the RP VM) and use option 1. The dbtoken service, which also runs on
the RP VM, is our implementation of option 2. As and when option 3 goes GA, it
may be possible to retire the dbtoken service.
The purpose of the dbtoken service at its implementation time is to enable the
gateway component (which handles end-user traffic) to access the service Cosmos
DB without recourse to using root credentials. This provides a level of defence
in depth in the face of an attack on the gateway component.
## Workflow
* An AAD application is manually created at rollout, registering the
https://dbtoken.aro.azure.com resource.
* The dbtoken service receives POST requests from any client wishing to receive
a scoped resource token at its /token?permission=<permission> endpoint.
* The dbtoken service validates that the POST request includes a valid
AAD-signed bearer JWT for the https://dbtoken.aro.azure.com resource. The
subject UUID is retrieved from the JWT.
* In the case of the gateway service, the JWT subject UUID is the UUID of the
service principal corresponding to the gateway VMSS MSI.
* Using its primary key Cosmos DB credential, the dbtoken requests a scoped
resource token for the given user UUID and <permission> from Cosmos DB and
proxies it to the caller.
* Clients may use the dbtoken.Refresher interface to handle regularly refreshing
the resource token and injecting it into the database client used by the rest
of the client codebase.
## Setup
* At rollout time, create an AAD application whose *Application ID URI*
(`identifierUris` in the application manifest) is
`https://dbtoken.aro.azure.com`. It is not necessary for the application to
have any permissions, credentials, etc.
* The dbtoken service is responsible for creating database users and permissions
- see the `ConfigurePermissions` function.

2
go.mod
Просмотреть файл

@ -52,7 +52,7 @@ require (
github.com/gorilla/sessions v1.2.1
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/h2non/filetype v1.1.1 // indirect
github.com/jim-minter/go-cosmosdb v0.0.0-20201119201311-b37af9b82812
github.com/jim-minter/go-cosmosdb v0.0.0-20210320020825-d7f11ed7bd6d
github.com/jstemmer/go-junit-report v0.9.1
github.com/leodido/go-urn v1.2.1 // indirect
github.com/libvirt/libvirt-go v7.0.0+incompatible // indirect

5
go.sum
Просмотреть файл

@ -777,7 +777,6 @@ github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzz
github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gobuffalo/flect v0.2.1 h1:GPoRjEN0QObosV4XwuoWvSd5uSiL0N3e91/xqyY4crQ=
github.com/gobuffalo/flect v0.2.1/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc=
github.com/gobuffalo/flect v0.2.2 h1:PAVD7sp0KOdfswjAw9BpLCU9hXo7wFSzgpQ+zNeks/A=
github.com/gobuffalo/flect v0.2.2/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc=
@ -1241,8 +1240,8 @@ github.com/jen20/awspolicyequivalence v1.1.0/go.mod h1:PV1fS2xyHhCLp83vbgSMFr2dr
github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/jim-minter/go-cosmosdb v0.0.0-20201119201311-b37af9b82812 h1:il0jxCpyWRQ5klfw8ey8yg+WCUdsZGjziYEk5rIDkuc=
github.com/jim-minter/go-cosmosdb v0.0.0-20201119201311-b37af9b82812/go.mod h1:n4wXKwl/rXS49qkPRFf3vovG0V6nkwAO4SbRwjGYibM=
github.com/jim-minter/go-cosmosdb v0.0.0-20210320020825-d7f11ed7bd6d h1:VH6BibAwhtDdFEOfEmRG77/RktPn/MdewL5QAokLlJA=
github.com/jim-minter/go-cosmosdb v0.0.0-20210320020825-d7f11ed7bd6d/go.mod h1:n4wXKwl/rXS49qkPRFf3vovG0V6nkwAO4SbRwjGYibM=
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a h1:GmsqmapfzSJkm28dhRoHz2tLRbJmqhU86IPgBtN3mmk=
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=

Просмотреть файл

@ -52,7 +52,12 @@ func run(ctx context.Context, log *logrus.Entry) error {
return err
}
dbc, err := database.NewDatabaseClient(ctx, log.WithField("component", "database"), _env, &noop.Noop{}, aead)
dbAuthorizer, err := database.NewMasterKeyAuthorizer(ctx, _env)
if err != nil {
return err
}
dbc, err := database.NewDatabaseClient(log.WithField("component", "database"), _env, dbAuthorizer, &noop.Noop{}, aead)
if err != nil {
return err
}

Просмотреть файл

@ -101,6 +101,10 @@ import_certs_secrets() {
--vault-name "$KEYVAULT_PREFIX-por" \
--name portal-server \
--file secrets/localhost.pem >/dev/null
az keyvault certificate import \
--vault-name "$KEYVAULT_PREFIX-dbt" \
--name dbtoken-server \
--file secrets/localhost.pem >/dev/null
az keyvault certificate import \
--vault-name "$KEYVAULT_PREFIX-por" \
--name portal-client \

Просмотреть файл

@ -140,10 +140,12 @@ func run(ctx context.Context, log *logrus.Entry) error {
_, _ = io.Copy(c2, c)
}()
defer func() {
_ = c.(*tls.Conn).CloseWrite()
func() {
defer func() {
_ = c.(*tls.Conn).CloseWrite()
}()
_, _ = io.Copy(c, c2)
}()
_, _ = io.Copy(c, c2)
<-ch
}(c)

Просмотреть файл

@ -52,7 +52,7 @@ func acceptableNames(path string) []string {
return []string{"mgmtredhatopenshift" + strings.ReplaceAll(m[1], "-", "")}
}
m = regexp.MustCompile(`^github.com/Azure/ARO-RP/pkg/(deploy|mirror|monitor|operator|portal)$`).FindStringSubmatch(path)
m = regexp.MustCompile(`^github.com/Azure/ARO-RP/pkg/(dbtoken|deploy|mirror|monitor|operator|portal)$`).FindStringSubmatch(path)
if m != nil {
return []string{"", "pkg" + m[1]}
}

Просмотреть файл

@ -26,7 +26,7 @@ type AsyncOperations interface {
// NewAsyncOperations returns a new AsyncOperations
func NewAsyncOperations(ctx context.Context, isLocalDevelopmentMode bool, dbc cosmosdb.DatabaseClient) (AsyncOperations, error) {
dbid, err := databaseName(isLocalDevelopmentMode)
dbid, err := Name(isLocalDevelopmentMode)
if err != nil {
return nil, err
}

Просмотреть файл

@ -30,7 +30,7 @@ type Billing interface {
// NewBilling returns a new Billing
func NewBilling(ctx context.Context, isLocalDevelopmentMode bool, dbc cosmosdb.DatabaseClient) (Billing, error) {
dbid, err := databaseName(isLocalDevelopmentMode)
dbid, err := Name(isLocalDevelopmentMode)
if err != nil {
return nil, err
}

Просмотреть файл

@ -1,5 +1,7 @@
//go:generate go run ../../../vendor/github.com/jim-minter/go-cosmosdb/cmd/gencosmosdb github.com/Azure/ARO-RP/pkg/api,AsyncOperationDocument github.com/Azure/ARO-RP/pkg/api,BillingDocument github.com/Azure/ARO-RP/pkg/api,MonitorDocument github.com/Azure/ARO-RP/pkg/api,OpenShiftClusterDocument github.com/Azure/ARO-RP/pkg/api,SubscriptionDocument
//go:generate go run ../../../vendor/golang.org/x/tools/cmd/goimports -local=github.com/Azure/ARO-RP -e -w ./
//go:generate go run ../../../vendor/github.com/golang/mock/mockgen -destination=../../util/mocks/$GOPACKAGE/$GOPACKAGE.go github.com/Azure/ARO-RP/pkg/database/$GOPACKAGE PermissionClient
//go:generate go run ../../../vendor/golang.org/x/tools/cmd/goimports -local=github.com/Azure/ARO-RP -e -w ../../util/mocks/$GOPACKAGE/$GOPACKAGE.go
package cosmosdb

Просмотреть файл

@ -0,0 +1,53 @@
// Code generated by github.com/jim-minter/go-cosmosdb, DO NOT EDIT.
package cosmosdb
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
type Authorizer interface {
Authorize(*http.Request, string, string)
}
type masterKeyAuthorizer struct {
masterKey []byte
}
func (a *masterKeyAuthorizer) Authorize(req *http.Request, resourceType, resourceLink string) {
date := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
h := hmac.New(sha256.New, a.masterKey)
fmt.Fprintf(h, "%s\n%s\n%s\n%s\n\n", strings.ToLower(req.Method), resourceType, resourceLink, strings.ToLower(date))
req.Header.Set("Authorization", url.QueryEscape(fmt.Sprintf("type=master&ver=1.0&sig=%s", base64.StdEncoding.EncodeToString(h.Sum(nil)))))
req.Header.Set("x-ms-date", date)
}
func NewMasterKeyAuthorizer(masterKey string) (Authorizer, error) {
b, err := base64.StdEncoding.DecodeString(masterKey)
if err != nil {
return nil, err
}
return &masterKeyAuthorizer{masterKey: b}, nil
}
type tokenAuthorizer struct {
token string
}
func (a *tokenAuthorizer) Authorize(req *http.Request, resourceType, resourceLink string) {
req.Header.Set("Authorization", url.QueryEscape(a.token))
}
func NewTokenAuthorizer(token string) Authorizer {
return &tokenAuthorizer{token: token}
}

Просмотреть файл

@ -23,6 +23,7 @@ type Collection struct {
PartitionKey *PartitionKey `json:"partitionKey,omitempty"`
UniqueKeyPolicy *UniqueKeyPolicy `json:"uniqueKeyPolicy,omitempty"`
ConflictResolutionPolicy *ConflictResolutionPolicy `json:"conflictResolutionPolicy,omitempty"`
AllowMaterializedViews bool `json:"allowMaterializedViews,omitempty"`
GeospatialConfig *GeospatialConfig `json:"geospatialConfig,omitempty"`
}

Просмотреть файл

@ -5,16 +5,11 @@ package cosmosdb
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/textproto"
"net/url"
"strconv"
"strings"
"time"
"github.com/ugorji/go/codec"
@ -69,16 +64,6 @@ func RetryOnPreconditionFailed(f func() error) (err error) {
return
}
func (c *databaseClient) authorizeRequest(req *http.Request, resourceType, resourceLink string) {
date := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
h := hmac.New(sha256.New, c.masterKey)
fmt.Fprintf(h, "%s\n%s\n%s\n%s\n\n", strings.ToLower(req.Method), resourceType, resourceLink, strings.ToLower(date))
req.Header.Set("Authorization", url.QueryEscape(fmt.Sprintf("type=master&ver=1.0&sig=%s", base64.StdEncoding.EncodeToString(h.Sum(nil)))))
req.Header.Set("x-ms-date", date)
}
func (c *databaseClient) do(ctx context.Context, method, path, resourceType, resourceLink string, expectedStatusCode int, in, out interface{}, headers http.Header) error {
var resp *http.Response
var err error
@ -133,7 +118,11 @@ func (c *databaseClient) _do(ctx context.Context, method, path, resourceType, re
req.Header.Set("x-ms-version", "2018-12-31")
c.authorizeRequest(req, resourceType, resourceLink)
c.mu.RLock()
if c.authorizer != nil {
c.authorizer.Authorize(req, resourceType, resourceLink)
}
c.mu.RUnlock()
resp, err := c.hc.Do(req)
if err != nil {

Просмотреть файл

@ -4,8 +4,8 @@ package cosmosdb
import (
"context"
"encoding/base64"
"net/http"
"sync"
"github.com/sirupsen/logrus"
"github.com/ugorji/go/codec"
@ -30,16 +30,18 @@ type Databases struct {
}
type databaseClient struct {
mu sync.RWMutex
log *logrus.Entry
hc *http.Client
jsonHandle *codec.JsonHandle
databaseHostname string
masterKey []byte
authorizer Authorizer
maxRetries int
}
// DatabaseClient is a database client
type DatabaseClient interface {
SetAuthorizer(Authorizer)
Create(context.Context, *Database) (*Database, error)
List() DatabaseIterator
ListAll(context.Context) (*Databases, error)
@ -59,23 +61,15 @@ type DatabaseIterator interface {
}
// NewDatabaseClient returns a new database client
func NewDatabaseClient(log *logrus.Entry, hc *http.Client, jsonHandle *codec.JsonHandle, databaseHostname, masterKey string) (DatabaseClient, error) {
var err error
c := &databaseClient{
func NewDatabaseClient(log *logrus.Entry, hc *http.Client, jsonHandle *codec.JsonHandle, databaseHostname string, authorizer Authorizer) DatabaseClient {
return &databaseClient{
log: log,
hc: hc,
jsonHandle: jsonHandle,
databaseHostname: databaseHostname,
authorizer: authorizer,
maxRetries: 10,
}
c.masterKey, err = base64.StdEncoding.DecodeString(masterKey)
if err != nil {
return nil, err
}
return c, nil
}
func (c *databaseClient) all(ctx context.Context, i DatabaseIterator) (*Databases, error) {
@ -98,6 +92,13 @@ func (c *databaseClient) all(ctx context.Context, i DatabaseIterator) (*Database
return alldbs, nil
}
func (c *databaseClient) SetAuthorizer(authorizer Authorizer) {
c.mu.Lock()
defer c.mu.Unlock()
c.authorizer = authorizer
}
func (c *databaseClient) Create(ctx context.Context, newdb *Database) (db *Database, err error) {
err = c.do(ctx, http.MethodPost, "dbs", "dbs", "", http.StatusCreated, &newdb, &db, nil)
return

Просмотреть файл

@ -0,0 +1,143 @@
// Code generated by github.com/jim-minter/go-cosmosdb, DO NOT EDIT.
package cosmosdb
import (
"context"
"net/http"
)
// Permission represents a permission
type Permission struct {
ID string `json:"id,omitempty"`
ResourceID string `json:"_rid,omitempty"`
Timestamp int `json:"_ts,omitempty"`
Self string `json:"_self,omitempty"`
ETag string `json:"_etag,omitempty"`
Token string `json:"_token,omitempty"`
PermissionMode PermissionMode `json:"permissionMode,omitempty"`
Resource string `json:"resource,omitempty"`
}
// PermissionMode represents a permission mode
type PermissionMode string
// PermissionMode constants
const (
PermissionModeAll PermissionMode = "All"
PermissionModeRead PermissionMode = "Read"
)
// Permissions represents permissions
type Permissions struct {
Count int `json:"_count,omitempty"`
ResourceID string `json:"_rid,omitempty"`
Permissions []*Permission `json:"Permissions,omitempty"`
}
type permissionClient struct {
*databaseClient
path string
}
// PermissionClient is a permission client
type PermissionClient interface {
Create(context.Context, *Permission) (*Permission, error)
List() PermissionIterator
ListAll(context.Context) (*Permissions, error)
Get(context.Context, string) (*Permission, error)
Delete(context.Context, *Permission) error
Replace(context.Context, *Permission) (*Permission, error)
}
type permissionListIterator struct {
*permissionClient
continuation string
done bool
}
// PermissionIterator is a permission iterator
type PermissionIterator interface {
Next(context.Context) (*Permissions, error)
}
// NewPermissionClient returns a new permission client
func NewPermissionClient(userc UserClient, userid string) PermissionClient {
return &permissionClient{
databaseClient: userc.(*userClient).databaseClient,
path: userc.(*userClient).path + "/users/" + userid,
}
}
func (c *permissionClient) all(ctx context.Context, i PermissionIterator) (*Permissions, error) {
allpermissions := &Permissions{}
for {
permissions, err := i.Next(ctx)
if err != nil {
return nil, err
}
if permissions == nil {
break
}
allpermissions.Count += permissions.Count
allpermissions.ResourceID = permissions.ResourceID
allpermissions.Permissions = append(allpermissions.Permissions, permissions.Permissions...)
}
return allpermissions, nil
}
func (c *permissionClient) Create(ctx context.Context, newpermission *Permission) (permission *Permission, err error) {
err = c.do(ctx, http.MethodPost, c.path+"/permissions", "permissions", c.path, http.StatusCreated, &newpermission, &permission, nil)
return
}
func (c *permissionClient) List() PermissionIterator {
return &permissionListIterator{permissionClient: c}
}
func (c *permissionClient) ListAll(ctx context.Context) (*Permissions, error) {
return c.all(ctx, c.List())
}
func (c *permissionClient) Get(ctx context.Context, permissionid string) (permission *Permission, err error) {
err = c.do(ctx, http.MethodGet, c.path+"/permissions/"+permissionid, "permissions", c.path+"/permissions/"+permissionid, http.StatusOK, nil, &permission, nil)
return
}
func (c *permissionClient) Delete(ctx context.Context, permission *Permission) error {
if permission.ETag == "" {
return ErrETagRequired
}
headers := http.Header{}
headers.Set("If-Match", permission.ETag)
return c.do(ctx, http.MethodDelete, c.path+"/permissions/"+permission.ID, "permissions", c.path+"/permissions/"+permission.ID, http.StatusNoContent, nil, nil, headers)
}
func (c *permissionClient) Replace(ctx context.Context, newpermission *Permission) (permission *Permission, err error) {
err = c.do(ctx, http.MethodPost, c.path+"/permissions/"+newpermission.ID, "permissions", c.path+"/permissions/"+newpermission.ID, http.StatusCreated, &newpermission, &permission, nil)
return
}
func (i *permissionListIterator) Next(ctx context.Context) (permissions *Permissions, err error) {
if i.done {
return
}
headers := http.Header{}
if i.continuation != "" {
headers.Set("X-Ms-Continuation", i.continuation)
}
err = i.do(ctx, http.MethodGet, i.path+"/permissions", "permissions", i.path, http.StatusOK, nil, &permissions, headers)
if err != nil {
return
}
i.continuation = headers.Get("X-Ms-Continuation")
i.done = i.continuation == ""
return
}

Просмотреть файл

@ -0,0 +1,132 @@
// Code generated by github.com/jim-minter/go-cosmosdb, DO NOT EDIT.
package cosmosdb
import (
"context"
"net/http"
)
// User represents a user
type User struct {
ID string `json:"id,omitempty"`
ResourceID string `json:"_rid,omitempty"`
Timestamp int `json:"_ts,omitempty"`
Self string `json:"_self,omitempty"`
ETag string `json:"_etag,omitempty"`
Permissions string `json:"_permissions,omitempty"`
}
// Users represents users
type Users struct {
Count int `json:"_count,omitempty"`
ResourceID string `json:"_rid,omitempty"`
Users []*User `json:"Users,omitempty"`
}
type userClient struct {
*databaseClient
path string
}
// UserClient is a user client
type UserClient interface {
Create(context.Context, *User) (*User, error)
List() UserIterator
ListAll(context.Context) (*Users, error)
Get(context.Context, string) (*User, error)
Delete(context.Context, *User) error
Replace(context.Context, *User) (*User, error)
}
type userListIterator struct {
*userClient
continuation string
done bool
}
// UserIterator is a user iterator
type UserIterator interface {
Next(context.Context) (*Users, error)
}
// NewUserClient returns a new user client
func NewUserClient(c DatabaseClient, dbid string) UserClient {
return &userClient{
databaseClient: c.(*databaseClient),
path: "dbs/" + dbid,
}
}
func (c *userClient) all(ctx context.Context, i UserIterator) (*Users, error) {
allusers := &Users{}
for {
users, err := i.Next(ctx)
if err != nil {
return nil, err
}
if users == nil {
break
}
allusers.Count += users.Count
allusers.ResourceID = users.ResourceID
allusers.Users = append(allusers.Users, users.Users...)
}
return allusers, nil
}
func (c *userClient) Create(ctx context.Context, newuser *User) (user *User, err error) {
err = c.do(ctx, http.MethodPost, c.path+"/users", "users", c.path, http.StatusCreated, &newuser, &user, nil)
return
}
func (c *userClient) List() UserIterator {
return &userListIterator{userClient: c}
}
func (c *userClient) ListAll(ctx context.Context) (*Users, error) {
return c.all(ctx, c.List())
}
func (c *userClient) Get(ctx context.Context, userid string) (user *User, err error) {
err = c.do(ctx, http.MethodGet, c.path+"/users/"+userid, "users", c.path+"/users/"+userid, http.StatusOK, nil, &user, nil)
return
}
func (c *userClient) Delete(ctx context.Context, user *User) error {
if user.ETag == "" {
return ErrETagRequired
}
headers := http.Header{}
headers.Set("If-Match", user.ETag)
return c.do(ctx, http.MethodDelete, c.path+"/users/"+user.ID, "users", c.path+"/users/"+user.ID, http.StatusNoContent, nil, nil, headers)
}
func (c *userClient) Replace(ctx context.Context, newuser *User) (user *User, err error) {
err = c.do(ctx, http.MethodPost, c.path+"/users/"+newuser.ID, "users", c.path+"/users/"+newuser.ID, http.StatusCreated, &newuser, &user, nil)
return
}
func (i *userListIterator) Next(ctx context.Context) (users *Users, err error) {
if i.done {
return
}
headers := http.Header{}
if i.continuation != "" {
headers.Set("X-Ms-Continuation", i.continuation)
}
err = i.do(ctx, http.MethodGet, i.path+"/users", "users", i.path, http.StatusOK, nil, &users, headers)
if err != nil {
return
}
i.continuation = headers.Get("X-Ms-Continuation")
i.done = i.continuation == ""
return
}

Просмотреть файл

@ -33,10 +33,13 @@ const (
collSubscriptions = "Subscriptions"
)
func NewDatabaseClient(ctx context.Context, log *logrus.Entry, env env.Core, m metrics.Interface, aead encryption.AEAD) (cosmosdb.DatabaseClient, error) {
databaseAccount, masterKey, err := find(ctx, env)
if err != nil {
return nil, err
func NewDatabaseClient(log *logrus.Entry, env env.Core, authorizer cosmosdb.Authorizer, m metrics.Interface, aead encryption.AEAD) (cosmosdb.DatabaseClient, error) {
for _, key := range []string{
"DATABASE_ACCOUNT_NAME",
} {
if _, found := os.LookupEnv(key); !found {
return nil, fmt.Errorf("environment variable %q unset", key)
}
}
h, err := NewJSONHandle(aead)
@ -53,8 +56,31 @@ func NewDatabaseClient(ctx context.Context, log *logrus.Entry, env env.Core, m m
Timeout: 30 * time.Second,
}
databaseHostname := databaseAccount + "." + env.Environment().CosmosDBDNSSuffix
return cosmosdb.NewDatabaseClient(log, c, h, databaseHostname, masterKey)
return cosmosdb.NewDatabaseClient(log, c, h, os.Getenv("DATABASE_ACCOUNT_NAME")+"."+env.Environment().CosmosDBDNSSuffix, authorizer), nil
}
func NewMasterKeyAuthorizer(ctx context.Context, env env.Core) (cosmosdb.Authorizer, error) {
for _, key := range []string{
"DATABASE_ACCOUNT_NAME",
} {
if _, found := os.LookupEnv(key); !found {
return nil, fmt.Errorf("environment variable %q unset", key)
}
}
rpAuthorizer, err := env.NewRPAuthorizer(env.Environment().ResourceManagerEndpoint)
if err != nil {
return nil, err
}
databaseaccounts := documentdb.NewDatabaseAccountsClient(env.Environment(), env.SubscriptionID(), rpAuthorizer)
keys, err := databaseaccounts.ListKeys(ctx, env.ResourceGroup(), os.Getenv("DATABASE_ACCOUNT_NAME"))
if err != nil {
return nil, err
}
return cosmosdb.NewMasterKeyAuthorizer(*keys.PrimaryMasterKey)
}
func NewJSONHandle(aead encryption.AEAD) (*codec.JsonHandle, error) {
@ -66,6 +92,10 @@ func NewJSONHandle(aead encryption.AEAD) (*codec.JsonHandle, error) {
},
}
if aead == nil {
return h, nil
}
err := h.SetInterfaceExt(reflect.TypeOf(api.SecureBytes{}), 1, secureBytesExt{aead: aead})
if err != nil {
return nil, err
@ -79,7 +109,7 @@ func NewJSONHandle(aead encryption.AEAD) (*codec.JsonHandle, error) {
return h, nil
}
func databaseName(isLocalDevelopmentMode bool) (string, error) {
func Name(isLocalDevelopmentMode bool) (string, error) {
if !isLocalDevelopmentMode {
return "ARO", nil
}
@ -94,29 +124,3 @@ func databaseName(isLocalDevelopmentMode bool) (string, error) {
return os.Getenv("DATABASE_NAME"), nil
}
func find(ctx context.Context, env env.Core) (string, string, error) {
for _, key := range []string{
"DATABASE_ACCOUNT_NAME",
} {
if _, found := os.LookupEnv(key); !found {
return "", "", fmt.Errorf("environment variable %q unset", key)
}
}
rpAuthorizer, err := env.NewRPAuthorizer(env.Environment().ResourceManagerEndpoint)
if err != nil {
return "", "", err
}
databaseaccounts := documentdb.NewDatabaseAccountsClient(env.Environment(), env.SubscriptionID(), rpAuthorizer)
acctName := os.Getenv("DATABASE_ACCOUNT_NAME")
keys, err := databaseaccounts.ListKeys(ctx, env.ResourceGroup(), acctName)
if err != nil {
return "", "", err
}
return acctName, *keys.PrimaryMasterKey, nil
}

Просмотреть файл

@ -32,7 +32,7 @@ type Monitors interface {
// NewMonitors returns a new Monitors
func NewMonitors(ctx context.Context, isLocalDevelopmentMode bool, dbc cosmosdb.DatabaseClient) (Monitors, error) {
dbid, err := databaseName(isLocalDevelopmentMode)
dbid, err := Name(isLocalDevelopmentMode)
if err != nil {
return nil, err
}

Просмотреть файл

@ -53,7 +53,7 @@ type OpenShiftClusters interface {
// NewOpenShiftClusters returns a new OpenShiftClusters
func NewOpenShiftClusters(ctx context.Context, isLocalDevelopmentMode bool, dbc cosmosdb.DatabaseClient) (OpenShiftClusters, error) {
dbid, err := databaseName(isLocalDevelopmentMode)
dbid, err := Name(isLocalDevelopmentMode)
if err != nil {
return nil, err
}

Просмотреть файл

@ -26,7 +26,7 @@ type Portal interface {
// NewPortal returns a new Portal
func NewPortal(ctx context.Context, isLocalDevelopmentMode bool, dbc cosmosdb.DatabaseClient) (Portal, error) {
dbid, err := databaseName(isLocalDevelopmentMode)
dbid, err := Name(isLocalDevelopmentMode)
if err != nil {
return nil, err
}

Просмотреть файл

@ -35,7 +35,7 @@ type Subscriptions interface {
// NewSubscriptions returns a new Subscriptions
func NewSubscriptions(ctx context.Context, isLocalDevelopmentMode bool, dbc cosmosdb.DatabaseClient) (Subscriptions, error) {
dbid, err := databaseName(isLocalDevelopmentMode)
dbid, err := Name(isLocalDevelopmentMode)
if err != nil {
return nil, err
}

10
pkg/dbtoken/api.go Normal file
Просмотреть файл

@ -0,0 +1,10 @@
package dbtoken
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
const Resource = "https://dbtoken.aro.azure.com/"
type tokenResponse struct {
Token string `json:"token,omitempty"`
}

104
pkg/dbtoken/client.go Normal file
Просмотреть файл

@ -0,0 +1,104 @@
package dbtoken
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/ARO-RP/pkg/env"
)
type Client interface {
Token(context.Context, string) (string, error)
}
type doer interface {
Do(*http.Request) (*http.Response, error)
}
type client struct {
c doer
authorizer autorest.Authorizer
url string
}
func NewClient(env env.Core, authorizer autorest.Authorizer, insecureSkipVerify bool) (Client, error) {
url := "https://localhost:8445"
if !env.IsLocalDevelopmentMode() {
for _, key := range []string{
"DBTOKEN_URL",
} {
if _, found := os.LookupEnv(key); !found {
return nil, fmt.Errorf("environment variable %q unset", key)
}
}
url = os.Getenv("DBTOKEN_URL")
}
return &client{
c: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecureSkipVerify,
},
// disable HTTP/2 for now: https://github.com/golang/go/issues/36026
TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{},
},
},
authorizer: authorizer,
url: url,
}, nil
}
func (c *client) Token(ctx context.Context, permission string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url+"/token", nil)
if err != nil {
return "", err
}
q := url.Values{
"permission": []string{permission},
}
req.URL.RawQuery = q.Encode()
var tr *tokenResponse
err = c.do(req, &tr)
if err != nil {
return "", err
}
return tr.Token, nil
}
func (c *client) do(req *http.Request, i interface{}) (err error) {
req, err = autorest.Prepare(req, c.authorizer.WithAuthorization())
if err != nil {
return err
}
resp, err := c.c.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
if resp.Header.Get("Content-Type") != "application/json" {
return fmt.Errorf("unexpected content type %q", resp.Header.Get("Content-Type"))
}
return json.NewDecoder(resp.Body).Decode(&i)
}

105
pkg/dbtoken/client_test.go Normal file
Просмотреть файл

@ -0,0 +1,105 @@
package dbtoken
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/Azure/go-autorest/autorest"
)
type fakeClient struct {
t *testing.T
wantMethod string
wantURL string
resp *http.Response
err error
}
func (fc *fakeClient) Do(req *http.Request) (*http.Response, error) {
if req.Method != fc.wantMethod {
fc.t.Fatal(req.Method)
}
if req.URL.String() != fc.wantURL {
fc.t.Fatal(req.URL.String())
}
return fc.resp, fc.err
}
func TestClient(t *testing.T) {
ctx := context.Background()
for _, tt := range []struct {
name string
fakeClient *fakeClient
wantToken string
wantErr string
}{
{
name: "works",
fakeClient: &fakeClient{
wantMethod: http.MethodPost,
wantURL: "https://localhost/token?permission=permission",
resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{
"Content-Type": []string{"application/json"},
},
Body: ioutil.NopCloser(strings.NewReader(`{"token":"token"}`)),
},
},
wantToken: "token",
},
{
name: "404",
fakeClient: &fakeClient{
wantMethod: http.MethodPost,
wantURL: "https://localhost/token?permission=permission",
resp: &http.Response{
StatusCode: http.StatusNotFound,
Body: ioutil.NopCloser(strings.NewReader("")),
},
},
wantErr: "unexpected status code 404",
},
{
name: "no content-type",
fakeClient: &fakeClient{
wantMethod: http.MethodPost,
wantURL: "https://localhost/token?permission=permission",
resp: &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader("")),
},
},
wantErr: `unexpected content type ""`,
},
} {
t.Run(tt.name, func(t *testing.T) {
tt.fakeClient.t = t
c := &client{
c: tt.fakeClient,
authorizer: &autorest.NullAuthorizer{},
url: "https://localhost",
}
token, err := c.Token(ctx, "permission")
if err != nil && err.Error() != tt.wantErr ||
err == nil && tt.wantErr != "" {
t.Fatal(err)
}
if token != tt.wantToken {
t.Error(token)
}
})
}
}

78
pkg/dbtoken/log.go Normal file
Просмотреть файл

@ -0,0 +1,78 @@
package dbtoken
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"io"
"net/http"
"time"
"github.com/sirupsen/logrus"
"github.com/Azure/ARO-RP/pkg/portal/middleware"
)
type logResponseWriter struct {
http.ResponseWriter
statusCode int
bytes int
}
func (w *logResponseWriter) Write(b []byte) (int, error) {
n, err := w.ResponseWriter.Write(b)
w.bytes += n
return n, err
}
func (w *logResponseWriter) WriteHeader(statusCode int) {
w.ResponseWriter.WriteHeader(statusCode)
w.statusCode = statusCode
}
type logReadCloser struct {
io.ReadCloser
bytes int
}
func (rc *logReadCloser) Read(b []byte) (int, error) {
n, err := rc.ReadCloser.Read(b)
rc.bytes += n
return n, err
}
func Log(log *logrus.Entry) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t := time.Now()
r.Body = &logReadCloser{ReadCloser: r.Body}
w = &logResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
username, _ := r.Context().Value(middleware.ContextKeyUsername).(string)
log := log.WithFields(logrus.Fields{
"request_method": r.Method,
"request_path": r.URL.Path,
"request_proto": r.Proto,
"request_remote_addr": r.RemoteAddr,
"request_user_agent": r.UserAgent(),
"username": username,
})
log.Print("read request")
defer func() {
log.WithFields(logrus.Fields{
"body_read_bytes": r.Body.(*logReadCloser).bytes,
"body_written_bytes": w.(*logResponseWriter).bytes,
"duration": time.Since(t).Seconds(),
"response_status_code": w.(*logResponseWriter).statusCode,
}).Print("sent response")
}()
h.ServeHTTP(w, r)
})
}
}

Просмотреть файл

@ -0,0 +1,14 @@
package dbtoken
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
)
func ConfigurePermissions(ctx context.Context, dbid string, userc cosmosdb.UserClient) error {
return nil
}

84
pkg/dbtoken/refresher.go Normal file
Просмотреть файл

@ -0,0 +1,84 @@
package dbtoken
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"sync/atomic"
"time"
"github.com/Azure/go-autorest/autorest"
"github.com/sirupsen/logrus"
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
"github.com/Azure/ARO-RP/pkg/env"
"github.com/Azure/ARO-RP/pkg/util/recover"
)
type Refresher interface {
Run(context.Context) error
Ready() bool
}
type refresher struct {
log *logrus.Entry
c Client
dbc cosmosdb.DatabaseClient
permission string
lastRefresh atomic.Value //time.Time
}
func NewRefresher(log *logrus.Entry, env env.Core, authorizer autorest.Authorizer, insecureSkipVerify bool, dbc cosmosdb.DatabaseClient, permission string) (Refresher, error) {
c, err := NewClient(env, authorizer, insecureSkipVerify)
if err != nil {
return nil, err
}
return &refresher{
log: log,
c: c,
dbc: dbc,
permission: permission,
}, nil
}
func (r *refresher) Run(ctx context.Context) error {
defer recover.Panic(r.log)
t := time.NewTicker(10 * time.Second)
defer t.Stop()
for {
err := r.runOnce(ctx)
if err != nil {
r.log.Error(err)
} else {
r.lastRefresh.Store(time.Now())
}
<-t.C
}
}
func (r *refresher) runOnce(ctx context.Context) error {
timeoutCtx, done := context.WithTimeout(ctx, time.Minute)
defer done()
token, err := r.c.Token(timeoutCtx, r.permission)
if err != nil {
return err
}
r.dbc.SetAuthorizer(cosmosdb.NewTokenAuthorizer(token))
return nil
}
func (r *refresher) Ready() bool {
lastRefresh, _ := r.lastRefresh.Load().(time.Time)
return time.Since(lastRefresh) < time.Hour
}

185
pkg/dbtoken/server.go Normal file
Просмотреть файл

@ -0,0 +1,185 @@
package dbtoken
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/json"
"log"
"net"
"net/http"
"regexp"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
"github.com/Azure/ARO-RP/pkg/env"
"github.com/Azure/ARO-RP/pkg/portal/middleware"
"github.com/Azure/ARO-RP/pkg/util/oidc"
)
var rxValidPermission = regexp.MustCompile("^[a-z]{1,20}$")
type Server interface {
Run(context.Context) error
}
type server struct {
env env.Core
log *logrus.Entry
accessLog *logrus.Entry
l net.Listener
verifier oidc.Verifier
permissionClientFactory func(userid string) cosmosdb.PermissionClient
}
func NewServer(
ctx context.Context,
env env.Core,
log *logrus.Entry,
accessLog *logrus.Entry,
l net.Listener,
servingKey *rsa.PrivateKey,
servingCerts []*x509.Certificate,
verifier oidc.Verifier,
userc cosmosdb.UserClient,
) (Server, error) {
config := &tls.Config{
Certificates: []tls.Certificate{
{
PrivateKey: servingKey,
},
},
NextProtos: []string{"h2", "http/1.1"},
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
},
PreferServerCipherSuites: true,
SessionTicketsDisabled: true,
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.CurveP256,
tls.X25519,
},
}
for _, cert := range servingCerts {
config.Certificates[0].Certificate = append(config.Certificates[0].Certificate, cert.Raw)
}
return &server{
env: env,
log: log,
accessLog: accessLog,
l: tls.NewListener(l, config),
verifier: verifier,
permissionClientFactory: func(userid string) cosmosdb.PermissionClient {
return cosmosdb.NewPermissionClient(userc, userid)
},
}, nil
}
func (s *server) Run(ctx context.Context) error {
r := mux.NewRouter()
r.Use(middleware.Panic(s.log))
unauthenticatedRouter := r.NewRoute().Subrouter()
unauthenticatedRouter.Use(Log(s.accessLog))
s.unauthenticatedRoutes(unauthenticatedRouter)
authenticatedRouter := r.NewRoute().Subrouter()
authenticatedRouter.Use(s.authenticate)
authenticatedRouter.Use(Log(s.accessLog))
s.authenticatedRoutes(authenticatedRouter)
srv := &http.Server{
Handler: r,
ReadTimeout: 10 * time.Second,
IdleTimeout: 2 * time.Minute,
ErrorLog: log.New(s.log.Writer(), "", 0),
BaseContext: func(net.Listener) context.Context { return ctx },
}
return srv.Serve(s.l)
}
func (s *server) unauthenticatedRoutes(r *mux.Router) {
r.NewRoute().Methods(http.MethodGet).Path("/healthz/ready").HandlerFunc(func(http.ResponseWriter, *http.Request) {})
}
func (s *server) authenticatedRoutes(r *mux.Router) {
r.NewRoute().Methods(http.MethodPost).Path("/token").HandlerFunc(s.token)
}
func (s *server) authenticate(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
token, err := s.verifier.Verify(ctx, strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "))
if err != nil {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
if _, err := uuid.FromString(token.Subject()); err != nil {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
ctx = context.WithValue(ctx, middleware.ContextKeyUsername, token.Subject())
r = r.WithContext(ctx)
h.ServeHTTP(w, r)
})
}
func (s *server) token(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
permission := r.URL.Query().Get("permission")
if !rxValidPermission.MatchString(permission) {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
username, _ := ctx.Value(middleware.ContextKeyUsername).(string)
permc := s.permissionClientFactory(username)
perm, err := permc.Get(ctx, permission)
if err != nil {
s.log.Error(err)
if cosmosdb.IsErrorStatusCode(err, http.StatusNotFound) {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", " ")
_ = e.Encode(&tokenResponse{
Token: perm.Token,
})
}

275
pkg/dbtoken/server_test.go Normal file
Просмотреть файл

@ -0,0 +1,275 @@
package dbtoken
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/url"
"testing"
"github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
mock_cosmosdb "github.com/Azure/ARO-RP/pkg/util/mocks/cosmosdb"
"github.com/Azure/ARO-RP/pkg/util/oidc"
"github.com/Azure/ARO-RP/test/util/listener"
)
func TestServer(t *testing.T) {
ctx := context.Background()
for _, tt := range []struct {
name string
permissionClientFactory func(controller *gomock.Controller) func(userid string) cosmosdb.PermissionClient
req *http.Request
wantStatusCode int
wantToken string
}{
{
name: "GET /random returns 404",
req: &http.Request{
Method: http.MethodGet,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/random",
},
},
wantStatusCode: http.StatusNotFound,
},
{
name: "GET /healthz/ready returns 200",
req: &http.Request{
Method: http.MethodGet,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/healthz/ready",
},
},
wantStatusCode: http.StatusOK,
},
{
name: "GET /token returns 405",
req: &http.Request{
Method: http.MethodGet,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/token",
},
},
wantStatusCode: http.StatusMethodNotAllowed,
},
{
name: "POST /token?permission=good returns 403 (no auth)",
req: &http.Request{
Method: http.MethodPost,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/token",
RawQuery: "permission=good",
},
},
wantStatusCode: http.StatusForbidden,
},
{
name: "POST /token?permission=good returns 403 (empty subject)",
req: &http.Request{
Method: http.MethodPost,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/token",
RawQuery: "permission=good",
},
Header: http.Header{
"Authorization": []string{`Bearer {"sub": ""}`},
},
},
wantStatusCode: http.StatusForbidden,
},
{
name: "POST /token?permission=good returns 403 (subject not UUID)",
req: &http.Request{
Method: http.MethodPost,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/token",
RawQuery: "permission=good",
},
Header: http.Header{
"Authorization": []string{`Bearer {"sub": "xyz"}`},
},
},
wantStatusCode: http.StatusForbidden,
},
{
name: "POST /token returns 400",
req: &http.Request{
Method: http.MethodPost,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/token",
},
Header: http.Header{
"Authorization": []string{`Bearer {"sub": "00000000-0000-0000-0000-000000000000"}`},
},
},
wantStatusCode: http.StatusBadRequest,
},
{
name: "POST /token?permission=bad! returns 400",
req: &http.Request{
Method: http.MethodPost,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/token",
RawQuery: "permission=bad!",
},
Header: http.Header{
"Authorization": []string{`Bearer {"sub": "00000000-0000-0000-0000-000000000000"}`},
},
},
wantStatusCode: http.StatusBadRequest,
},
{
name: "POST /token?permission=notexist returns 400",
permissionClientFactory: func(controller *gomock.Controller) func(userid string) cosmosdb.PermissionClient {
return func(userid string) cosmosdb.PermissionClient {
permc := mock_cosmosdb.NewMockPermissionClient(controller)
permc.EXPECT().Get(gomock.Any(), "notexist").Return(nil, &cosmosdb.Error{StatusCode: http.StatusNotFound})
return permc
}
},
req: &http.Request{
Method: http.MethodPost,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/token",
RawQuery: "permission=notexist",
},
Header: http.Header{
"Authorization": []string{`Bearer {"sub": "00000000-0000-0000-0000-000000000000"}`},
},
},
wantStatusCode: http.StatusBadRequest,
},
{
name: "POST /token?permission=perm and database error returns 500",
permissionClientFactory: func(controller *gomock.Controller) func(userid string) cosmosdb.PermissionClient {
return func(userid string) cosmosdb.PermissionClient {
permc := mock_cosmosdb.NewMockPermissionClient(controller)
permc.EXPECT().Get(gomock.Any(), "perm").Return(nil, errors.New("sad database"))
return permc
}
},
req: &http.Request{
Method: http.MethodPost,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/token",
RawQuery: "permission=perm",
},
Header: http.Header{
"Authorization": []string{`Bearer {"sub": "00000000-0000-0000-0000-000000000000"}`},
},
},
wantStatusCode: http.StatusInternalServerError,
},
{
name: "POST /token?permission=perm returns 200",
permissionClientFactory: func(controller *gomock.Controller) func(userid string) cosmosdb.PermissionClient {
return func(userid string) cosmosdb.PermissionClient {
permc := mock_cosmosdb.NewMockPermissionClient(controller)
permc.EXPECT().Get(gomock.Any(), "perm").Return(&cosmosdb.Permission{
Token: "token",
}, nil)
return permc
}
},
req: &http.Request{
Method: http.MethodPost,
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/token",
RawQuery: "permission=perm",
},
Header: http.Header{
"Authorization": []string{`Bearer {"sub": "00000000-0000-0000-0000-000000000000"}`},
},
},
wantStatusCode: http.StatusOK,
wantToken: "token",
},
} {
t.Run(tt.name, func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
l := listener.NewListener()
defer l.Close()
s := &server{
log: logrus.NewEntry(logrus.StandardLogger()),
accessLog: logrus.NewEntry(logrus.StandardLogger()),
l: l,
verifier: &oidc.NoopVerifier{},
}
if tt.permissionClientFactory != nil {
s.permissionClientFactory = tt.permissionClientFactory(controller)
}
go func() {
_ = s.Run(ctx)
}()
c := &http.Client{
Transport: &http.Transport{
DialContext: l.DialContext,
},
}
resp, err := c.Do(tt.req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != tt.wantStatusCode {
t.Error(resp.StatusCode)
}
if tt.wantToken == "" {
return
}
if resp.Header.Get("Content-Type") != "application/json" {
t.Fatal(resp.Header.Get("Content-Type"))
}
var tr *tokenResponse
err = json.NewDecoder(resp.Body).Decode(&tr)
if err != nil {
t.Fatal(err)
}
if tr.Token != tt.wantToken {
t.Error(tr.Token)
}
})
}
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -46,6 +46,7 @@ type Configuration struct {
ClusterParentDomainName *string `json:"clusterParentDomainName,omitempty" value:"required"`
DatabaseAccountName *string `json:"databaseAccountName,omitempty" value:"required"`
ExtraClusterKeyvaultAccessPolicies []interface{} `json:"extraClusterKeyvaultAccessPolicies,omitempty" value:"required"`
ExtraDBTokenKeyvaultAccessPolicies []interface{} `json:"extraDBTokenKeyvaultAccessPolicies,omitempty" value:"required"`
ExtraCosmosDBIPs []string `json:"extraCosmosDBIPs,omitempty"`
ExtraPortalKeyvaultAccessPolicies []interface{} `json:"extraPortalKeyvaultAccessPolicies,omitempty" value:"required"`
ExtraServiceKeyvaultAccessPolicies []interface{} `json:"extraServiceKeyvaultAccessPolicies,omitempty" value:"required"`

Просмотреть файл

@ -43,6 +43,7 @@ type deployer struct {
deployments features.DeploymentsClient
features features.Client
groups features.ResourceGroupsClient
loadbalancers network.LoadBalancersClient
userassignedidentities msi.UserAssignedIdentitiesClient
providers features.ProvidersClient
publicipaddresses network.PublicIPAddressesClient
@ -86,6 +87,7 @@ func New(ctx context.Context, log *logrus.Entry, env env.Core, config *RPConfig,
deployments: features.NewDeploymentsClient(env.Environment(), config.SubscriptionID, authorizer),
features: features.NewClient(env.Environment(), config.SubscriptionID, authorizer),
groups: features.NewResourceGroupsClient(env.Environment(), config.SubscriptionID, authorizer),
loadbalancers: network.NewLoadBalancersClient(env.Environment(), config.SubscriptionID, authorizer),
userassignedidentities: msi.NewUserAssignedIdentitiesClient(env.Environment(), config.SubscriptionID, authorizer),
providers: features.NewProvidersClient(env.Environment(), config.SubscriptionID, authorizer),
roleassignments: authorization.NewRoleAssignmentsClient(env.Environment(), config.SubscriptionID, authorizer),

Просмотреть файл

@ -111,6 +111,12 @@ func (d *deployer) configureDNS(ctx context.Context) error {
return err
}
lb, err := d.loadbalancers.Get(ctx, d.config.RPResourceGroupName, "rp-lb-internal", "")
if err != nil {
return err
}
dbtokenIp := *((*lb.FrontendIPConfigurations)[0].PrivateIPAddress)
zone, err := d.zones.Get(ctx, d.config.RPResourceGroupName, d.config.Location+"."+*d.config.Configuration.ClusterParentDomainName)
if err != nil {
return err
@ -144,6 +150,20 @@ func (d *deployer) configureDNS(ctx context.Context) error {
return err
}
_, err = d.globalrecordsets.CreateOrUpdate(ctx, *d.config.Configuration.GlobalResourceGroupName, *d.config.Configuration.RPParentDomainName, "dbtoken."+d.config.Location, mgmtdns.A, mgmtdns.RecordSet{
RecordSetProperties: &mgmtdns.RecordSetProperties{
TTL: to.Int64Ptr(3600),
ARecords: &[]mgmtdns.ARecord{
{
Ipv4Address: &dbtokenIp,
},
},
},
}, "", "")
if err != nil {
return err
}
nsRecords := make([]mgmtdns.NsRecord, 0, len(*zone.NameServers))
for i := range *zone.NameServers {
nsRecords = append(nsRecords, mgmtdns.NsRecord{

Просмотреть файл

@ -111,6 +111,9 @@ func DevConfig(_env env.Core) (*Config, error) {
ExtraClusterKeyvaultAccessPolicies: []interface{}{
adminKeyvaultAccessPolicy(_env),
},
ExtraDBTokenKeyvaultAccessPolicies: []interface{}{
adminKeyvaultAccessPolicy(_env),
},
ExtraPortalKeyvaultAccessPolicies: []interface{}{
adminKeyvaultAccessPolicy(_env),
deployKeyvaultAccessPolicy(_env),

Просмотреть файл

@ -263,6 +263,68 @@ func (g *generator) rpLB() *arm.Resource {
}
}
func (g *generator) rpLBInternal() *arm.Resource {
return &arm.Resource{
Resource: &mgmtnetwork.LoadBalancer{
Sku: &mgmtnetwork.LoadBalancerSku{
Name: mgmtnetwork.LoadBalancerSkuNameStandard,
},
LoadBalancerPropertiesFormat: &mgmtnetwork.LoadBalancerPropertiesFormat{
FrontendIPConfigurations: &[]mgmtnetwork.FrontendIPConfiguration{
{
FrontendIPConfigurationPropertiesFormat: &mgmtnetwork.FrontendIPConfigurationPropertiesFormat{
Subnet: &mgmtnetwork.Subnet{
ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'rp-vnet', 'rp-subnet')]"),
},
},
Name: to.StringPtr("dbtoken-frontend"),
},
},
BackendAddressPools: &[]mgmtnetwork.BackendAddressPool{
{
Name: to.StringPtr("rp-backend"),
},
},
LoadBalancingRules: &[]mgmtnetwork.LoadBalancingRule{
{
LoadBalancingRulePropertiesFormat: &mgmtnetwork.LoadBalancingRulePropertiesFormat{
FrontendIPConfiguration: &mgmtnetwork.SubResource{
ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', 'rp-lb-internal', 'dbtoken-frontend')]"),
},
BackendAddressPool: &mgmtnetwork.SubResource{
ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb-internal', 'rp-backend')]"),
},
Probe: &mgmtnetwork.SubResource{
ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/probes', 'rp-lb-internal', 'dbtoken-probe')]"),
},
Protocol: mgmtnetwork.TransportProtocolTCP,
LoadDistribution: mgmtnetwork.LoadDistributionDefault,
FrontendPort: to.Int32Ptr(443),
BackendPort: to.Int32Ptr(445),
},
Name: to.StringPtr("dbtoken-lbrule"),
},
},
Probes: &[]mgmtnetwork.Probe{
{
ProbePropertiesFormat: &mgmtnetwork.ProbePropertiesFormat{
Protocol: mgmtnetwork.ProbeProtocolHTTPS,
Port: to.Int32Ptr(445),
NumberOfProbes: to.Int32Ptr(2),
RequestPath: to.StringPtr("/healthz/ready"),
},
Name: to.StringPtr("dbtoken-probe"),
},
},
},
Name: to.StringPtr("rp-lb-internal"),
Type: to.StringPtr("Microsoft.Network/loadBalancers"),
Location: to.StringPtr("[resourceGroup().location]"),
},
APIVersion: azureclient.APIVersion("Microsoft.Network"),
}
}
// rpLBAlert generates an alert resource for the rp-lb healthprobe metric
func (g *generator) rpLBAlert(threshold float64, severity int32, name string, evalFreq string, windowSize string, metric string) *arm.Resource {
return &arm.Resource{
@ -430,6 +492,7 @@ sysctl --system
firewall-cmd --add-port=443/tcp --permanent
firewall-cmd --add-port=444/tcp --permanent
firewall-cmd --add-port=445/tcp --permanent
firewall-cmd --add-port=2222/tcp --permanent
cat >/etc/td-agent-bit/td-agent-bit.conf <<'EOF'
@ -578,6 +641,46 @@ StartLimitInterval=0
WantedBy=multi-user.target
EOF
cat >/etc/sysconfig/aro-dbtoken <<EOF
DATABASE_ACCOUNT_NAME='$DATABASEACCOUNTNAME'
KEYVAULT_PREFIX='$KEYVAULTPREFIX'
MDM_ACCOUNT=AzureRedHatOpenShiftRP
MDM_NAMESPACE=DBToken
RPIMAGE='$RPIMAGE'
EOF
cat >/etc/systemd/system/aro-dbtoken.service <<'EOF'
[Unit]
After=docker.service
Requires=docker.service
[Service]
EnvironmentFile=/etc/sysconfig/aro-dbtoken
ExecStartPre=-/usr/bin/docker rm -f %N
ExecStart=/usr/bin/docker run \
--hostname %H \
--name %N \
--rm \
-e DATABASE_ACCOUNT_NAME \
-e KEYVAULT_PREFIX \
-e MDM_ACCOUNT \
-e MDM_NAMESPACE \
-m 2g \
-p 445:8445 \
-v /run/systemd/journal:/run/systemd/journal \
-v /var/etw:/var/etw:z \
$RPIMAGE \
dbtoken
ExecStop=/usr/bin/docker stop -t 3600 %N
TimeoutStopSec=3600
Restart=always
RestartSec=1
StartLimitInterval=0
[Install]
WantedBy=multi-user.target
EOF
cat >/etc/sysconfig/aro-monitor <<EOF
CLUSTER_MDM_ACCOUNT=AzureRedHatOpenShiftCluster
CLUSTER_MDM_NAMESPACE=BBM
@ -781,7 +884,7 @@ mkdir -p /usr/lib/ssl/certs
csplit -f /usr/lib/ssl/certs/cert- -b %03d.pem /etc/pki/tls/certs/ca-bundle.crt /^$/1 {*} >/dev/null
c_rehash /usr/lib/ssl/certs
for service in aro-monitor aro-portal aro-rp auoms azsecd azsecmond mdsd mdm chronyd td-agent-bit; do
for service in aro-dbtoken aro-monitor aro-portal aro-rp auoms azsecd azsecmond mdsd mdm chronyd td-agent-bit; do
systemctl enable $service.service
done
@ -861,6 +964,9 @@ done
{
ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb', 'rp-backend')]"),
},
{
ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb-internal', 'rp-backend')]"),
},
},
},
},
@ -913,6 +1019,7 @@ done
"[resourceId('Microsoft.Authorization/roleAssignments', guid(resourceGroup().id, parameters('rpServicePrincipalId'), 'RP / Reader'))]",
"[resourceId('Microsoft.Network/virtualNetworks', 'rp-vnet')]",
"[resourceId('Microsoft.Network/loadBalancers', 'rp-lb')]",
"[resourceId('Microsoft.Network/loadBalancers', 'rp-lb-internal')]",
"[resourceId('Microsoft.Storage/storageAccounts', substring(parameters('storageAccountDomain'), 0, indexOf(parameters('storageAccountDomain'), '.')))]",
},
}
@ -966,6 +1073,20 @@ func (g *generator) rpClusterKeyvaultAccessPolicies() []mgmtkeyvault.AccessPolic
}
}
func (g *generator) rpDBTokenKeyvaultAccessPolicies() []mgmtkeyvault.AccessPolicyEntry {
return []mgmtkeyvault.AccessPolicyEntry{
{
TenantID: &tenantUUIDHack,
ObjectID: to.StringPtr("[parameters('rpServicePrincipalId')]"),
Permissions: &mgmtkeyvault.Permissions{
Secrets: &[]mgmtkeyvault.SecretPermissions{
mgmtkeyvault.SecretPermissionsGet,
},
},
},
}
}
func (g *generator) rpPortalKeyvaultAccessPolicies() []mgmtkeyvault.AccessPolicyEntry {
return []mgmtkeyvault.AccessPolicyEntry{
{
@ -1035,6 +1156,53 @@ func (g *generator) rpClusterKeyvault() *arm.Resource {
}
}
func (g *generator) rpDBTokenKeyvault() *arm.Resource {
vault := &mgmtkeyvault.Vault{
Properties: &mgmtkeyvault.VaultProperties{
EnableSoftDelete: to.BoolPtr(true),
TenantID: &tenantUUIDHack,
Sku: &mgmtkeyvault.Sku{
Name: mgmtkeyvault.Standard,
Family: to.StringPtr("A"),
},
AccessPolicies: &[]mgmtkeyvault.AccessPolicyEntry{
{
ObjectID: to.StringPtr(dbTokenAccessPolicyHack),
},
},
},
Name: to.StringPtr("[concat(parameters('keyvaultPrefix'), '" + env.DBTokenKeyvaultSuffix + "')]"),
Type: to.StringPtr("Microsoft.KeyVault/vaults"),
Location: to.StringPtr("[resourceGroup().location]"),
}
if !g.production {
*vault.Properties.AccessPolicies = append(g.rpDBTokenKeyvaultAccessPolicies(),
mgmtkeyvault.AccessPolicyEntry{
TenantID: &tenantUUIDHack,
ObjectID: to.StringPtr("[parameters('adminObjectId')]"),
Permissions: &mgmtkeyvault.Permissions{
Certificates: &[]mgmtkeyvault.CertificatePermissions{
mgmtkeyvault.Delete,
mgmtkeyvault.Get,
mgmtkeyvault.Import,
mgmtkeyvault.List,
},
Secrets: &[]mgmtkeyvault.SecretPermissions{
mgmtkeyvault.SecretPermissionsSet,
mgmtkeyvault.SecretPermissionsList,
},
},
},
)
}
return &arm.Resource{
Resource: vault,
APIVersion: azureclient.APIVersion("Microsoft.KeyVault"),
}
}
func (g *generator) rpPortalKeyvault() *arm.Resource {
vault := &mgmtkeyvault.Vault{
Properties: &mgmtkeyvault.VaultProperties{

Просмотреть файл

@ -16,6 +16,7 @@ import (
const (
tenantIDHack = "13805ec3-a223-47ad-ad65-8b2baf92c0fb"
clusterAccessPolicyHack = "e1992efe-4835-46cf-8c08-d8b8451044b8"
dbTokenAccessPolicyHack = "bb6c76fd-76ea-43c9-8ee3-ca568ae1c226"
portalAccessPolicyHack = "e5e11dae-7c49-4118-9628-e0afa4d6a502"
serviceAccessPolicyHack = "533a94d0-d6c2-4fca-9af1-374aa6493468"
)
@ -46,6 +47,7 @@ func (g *generator) templateFixup(t *arm.Template) ([]byte, error) {
b = bytes.ReplaceAll(b, []byte(`"capacity": 1338`), []byte(`"capacity": "[parameters('rpVmssCapacity')]"`))
if g.production {
b = regexp.MustCompile(`(?m)"accessPolicies": \[[^]]*`+clusterAccessPolicyHack+`[^]]*\]`).ReplaceAll(b, []byte(`"accessPolicies": "[concat(variables('clusterKeyvaultAccessPolicies'), parameters('extraClusterKeyvaultAccessPolicies'))]"`))
b = regexp.MustCompile(`(?m)"accessPolicies": \[[^]]*`+dbTokenAccessPolicyHack+`[^]]*\]`).ReplaceAll(b, []byte(`"accessPolicies": "[concat(variables('dbTokenKeyvaultAccessPolicies'), parameters('extraDBTokenKeyvaultAccessPolicies'))]"`))
b = regexp.MustCompile(`(?m)"accessPolicies": \[[^]]*`+portalAccessPolicyHack+`[^]]*\]`).ReplaceAll(b, []byte(`"accessPolicies": "[concat(variables('portalKeyvaultAccessPolicies'), parameters('extraPortalKeyvaultAccessPolicies'))]"`))
b = regexp.MustCompile(`(?m)"accessPolicies": \[[^]]*`+serviceAccessPolicyHack+`[^]]*\]`).ReplaceAll(b, []byte(`"accessPolicies": "[concat(variables('serviceKeyvaultAccessPolicies'), parameters('extraServiceKeyvaultAccessPolicies'))]"`))
b = bytes.Replace(b, []byte(`"sourceAddressPrefixes": []`), []byte(`"sourceAddressPrefixes": "[parameters('rpNsgSourceAddressPrefixes')]"`), 1)

Просмотреть файл

@ -87,6 +87,7 @@ func (g *generator) rpTemplate() *arm.Template {
g.publicIPAddress("rp-pip"),
g.publicIPAddress("portal-pip"),
g.rpLB(),
g.rpLBInternal(),
g.rpVMSS(),
g.rpStorageAccount(),
g.rpLBAlert(30.0, 2, "rp-availability-alert", "PT5M", "PT15M", "DipAvailability"), // triggers on all 3 RPs being down for 10min, can't be >=0.3 due to deploys going down to 32% at times.
@ -199,6 +200,7 @@ func (g *generator) rpPredeployTemplate() *arm.Template {
if g.production {
t.Variables = map[string]interface{}{
"clusterKeyvaultAccessPolicies": g.rpClusterKeyvaultAccessPolicies(),
"dbTokenKeyvaultAccessPolicies": g.rpDBTokenKeyvaultAccessPolicies(),
"portalKeyvaultAccessPolicies": g.rpPortalKeyvaultAccessPolicies(),
"serviceKeyvaultAccessPolicies": g.rpServiceKeyvaultAccessPolicies(),
}
@ -214,6 +216,7 @@ func (g *generator) rpPredeployTemplate() *arm.Template {
params = append(params,
"deployNSGs",
"extraClusterKeyvaultAccessPolicies",
"extraDBTokenKeyvaultAccessPolicies",
"extraPortalKeyvaultAccessPolicies",
"extraServiceKeyvaultAccessPolicies",
"rpNsgSourceAddressPrefixes",
@ -231,6 +234,7 @@ func (g *generator) rpPredeployTemplate() *arm.Template {
p.Type = "bool"
p.DefaultValue = false
case "extraClusterKeyvaultAccessPolicies",
"extraDBTokenKeyvaultAccessPolicies",
"extraPortalKeyvaultAccessPolicies",
"extraServiceKeyvaultAccessPolicies":
p.Type = "array"
@ -248,6 +252,7 @@ func (g *generator) rpPredeployTemplate() *arm.Template {
g.rpSecurityGroup(),
g.rpPESecurityGroup(),
g.rpClusterKeyvault(),
g.rpDBTokenKeyvault(),
g.rpPortalKeyvault(),
g.rpServiceKeyvault(),
)

2
pkg/env/env.go поставляемый
Просмотреть файл

@ -37,6 +37,7 @@ const (
ClusterLoggingSecretName = "cluster-mdsd"
EncryptionSecretName = "encryption-key"
FrontendEncryptionSecretName = "fe-encryption-key"
DBTokenServerSecretName = "dbtoken-server"
RPLoggingSecretName = "rp-mdsd"
RPMonitoringSecretName = "rp-mdm"
PortalServerSecretName = "portal-server"
@ -44,6 +45,7 @@ const (
PortalServerSessionKeySecretName = "portal-session-key"
PortalServerSSHKeySecretName = "portal-sshkey"
ClusterKeyvaultSuffix = "-cls"
DBTokenKeyvaultSuffix = "-dbt"
PortalKeyvaultSuffix = "-por"
ServiceKeyvaultSuffix = "-svc"
RPPrivateEndpointPrefix = "rp-pe-"

Просмотреть файл

@ -15,7 +15,6 @@ import (
"time"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/coreos/go-oidc"
"github.com/gofrs/uuid"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
@ -24,6 +23,7 @@ import (
"golang.org/x/oauth2/microsoft"
"github.com/Azure/ARO-RP/pkg/env"
"github.com/Azure/ARO-RP/pkg/util/oidc"
"github.com/Azure/ARO-RP/pkg/util/roundtripper"
)
@ -52,35 +52,6 @@ type oauther interface {
Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error)
}
type Verifier interface {
Verify(context.Context, string) (oidctoken, error)
}
type idTokenVerifier struct {
*oidc.IDTokenVerifier
}
func (v *idTokenVerifier) Verify(ctx context.Context, rawIDToken string) (oidctoken, error) {
return v.IDTokenVerifier.Verify(ctx, rawIDToken)
}
type oidctoken interface {
Claims(interface{}) error
}
func NewVerifier(ctx context.Context, env env.Core, clientID string) (Verifier, error) {
provider, err := oidc.NewProvider(ctx, env.Environment().ActiveDirectoryEndpoint+env.TenantID()+"/v2.0")
if err != nil {
return nil, err
}
return &idTokenVerifier{
provider.Verifier(&oidc.Config{
ClientID: clientID,
}),
}, nil
}
type claims struct {
Groups []string `json:"groups,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
@ -99,7 +70,7 @@ type aad struct {
store *sessions.CookieStore
oauther oauther
verifier Verifier
verifier oidc.Verifier
allGroups []string
sessionTimeout time.Duration
@ -116,7 +87,7 @@ func NewAAD(log *logrus.Entry,
clientCerts []*x509.Certificate,
allGroups []string,
unauthenticatedRouter *mux.Router,
verifier Verifier) (AAD, error) {
verifier oidc.Verifier) (AAD, error) {
if len(sessionKey) != 32 {
return nil, errors.New("invalid sessionKey")
}

Просмотреть файл

@ -26,6 +26,7 @@ import (
"golang.org/x/oauth2"
mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
"github.com/Azure/ARO-RP/pkg/util/oidc"
"github.com/Azure/ARO-RP/pkg/util/roundtripper"
utiltls "github.com/Azure/ARO-RP/pkg/util/tls"
testlog "github.com/Azure/ARO-RP/test/util/log"
@ -63,23 +64,6 @@ func (o *noopOauther) Exchange(context.Context, string, ...oauth2.AuthCodeOption
return t.WithExtra(o.tokenMap), nil
}
type noopVerifier struct {
err error
}
func (v *noopVerifier) Verify(ctx context.Context, rawtoken string) (oidctoken, error) {
if v.err != nil {
return nil, v.err
}
return noopClaims(rawtoken), nil
}
type noopClaims []byte
func (c noopClaims) Claims(v interface{}) error {
return json.Unmarshal(c, v)
}
func TestNewAAD(t *testing.T) {
_, err := NewAAD(nil, nil, nil, nil, "", nil, "", nil, nil, nil, nil, nil)
if err.Error() != "invalid sessionKey" {
@ -444,7 +428,7 @@ func TestCallback(t *testing.T) {
name string
request func(*aad) (*http.Request, error)
oauther oauther
verifier Verifier
verifier oidc.Verifier
wantAuthenticated bool
wantError string
wantForbidden bool
@ -477,7 +461,7 @@ func TestCallback(t *testing.T) {
"id_token": string(idToken),
},
},
verifier: &noopVerifier{},
verifier: &oidc.NoopVerifier{},
wantAuthenticated: true,
},
{
@ -637,8 +621,8 @@ func TestCallback(t *testing.T) {
oauther: &noopOauther{
tokenMap: map[string]interface{}{"id_token": ""},
},
verifier: &noopVerifier{
err: fmt.Errorf("failed"),
verifier: &oidc.NoopVerifier{
Err: fmt.Errorf("failed"),
},
wantError: "Internal Server Error\n",
},
@ -671,7 +655,7 @@ func TestCallback(t *testing.T) {
"id_token": "",
},
},
verifier: &noopVerifier{},
verifier: &oidc.NoopVerifier{},
wantError: "Internal Server Error\n",
},
{
@ -703,7 +687,7 @@ func TestCallback(t *testing.T) {
"id_token": "null",
},
},
verifier: &noopVerifier{},
verifier: &oidc.NoopVerifier{},
wantForbidden: true,
},
{
@ -734,7 +718,7 @@ func TestCallback(t *testing.T) {
"id_token": string(idToken),
},
},
verifier: &noopVerifier{},
verifier: &oidc.NoopVerifier{},
wantError: "Internal Server Error\n",
},
} {

Просмотреть файл

@ -30,6 +30,7 @@ import (
"github.com/Azure/ARO-RP/pkg/portal/prometheus"
"github.com/Azure/ARO-RP/pkg/portal/ssh"
"github.com/Azure/ARO-RP/pkg/proxy"
"github.com/Azure/ARO-RP/pkg/util/oidc"
)
type Runnable interface {
@ -43,7 +44,7 @@ type portal struct {
baseAccessLog *logrus.Entry
l net.Listener
sshl net.Listener
verifier middleware.Verifier
verifier oidc.Verifier
hostname string
servingKey *rsa.PrivateKey
@ -73,7 +74,7 @@ func NewPortal(env env.Core,
baseAccessLog *logrus.Entry,
l net.Listener,
sshl net.Listener,
verifier middleware.Verifier,
verifier oidc.Verifier,
hostname string,
servingKey *rsa.PrivateKey,
servingCerts []*x509.Certificate,

Просмотреть файл

@ -3,7 +3,9 @@ package azureclaim
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import "fmt"
import (
"fmt"
)
type AzureClaim struct {
Roles []string `json:"roles,omitempty"`

Просмотреть файл

@ -0,0 +1,125 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/Azure/ARO-RP/pkg/database/cosmosdb (interfaces: PermissionClient)
// Package mock_cosmosdb is a generated GoMock package.
package mock_cosmosdb
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
cosmosdb "github.com/Azure/ARO-RP/pkg/database/cosmosdb"
)
// MockPermissionClient is a mock of PermissionClient interface
type MockPermissionClient struct {
ctrl *gomock.Controller
recorder *MockPermissionClientMockRecorder
}
// MockPermissionClientMockRecorder is the mock recorder for MockPermissionClient
type MockPermissionClientMockRecorder struct {
mock *MockPermissionClient
}
// NewMockPermissionClient creates a new mock instance
func NewMockPermissionClient(ctrl *gomock.Controller) *MockPermissionClient {
mock := &MockPermissionClient{ctrl: ctrl}
mock.recorder = &MockPermissionClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPermissionClient) EXPECT() *MockPermissionClientMockRecorder {
return m.recorder
}
// Create mocks base method
func (m *MockPermissionClient) Create(arg0 context.Context, arg1 *cosmosdb.Permission) (*cosmosdb.Permission, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", arg0, arg1)
ret0, _ := ret[0].(*cosmosdb.Permission)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create
func (mr *MockPermissionClientMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockPermissionClient)(nil).Create), arg0, arg1)
}
// Delete mocks base method
func (m *MockPermissionClient) Delete(arg0 context.Context, arg1 *cosmosdb.Permission) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockPermissionClientMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPermissionClient)(nil).Delete), arg0, arg1)
}
// Get mocks base method
func (m *MockPermissionClient) Get(arg0 context.Context, arg1 string) (*cosmosdb.Permission, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0, arg1)
ret0, _ := ret[0].(*cosmosdb.Permission)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockPermissionClientMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPermissionClient)(nil).Get), arg0, arg1)
}
// List mocks base method
func (m *MockPermissionClient) List() cosmosdb.PermissionIterator {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].(cosmosdb.PermissionIterator)
return ret0
}
// List indicates an expected call of List
func (mr *MockPermissionClientMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPermissionClient)(nil).List))
}
// ListAll mocks base method
func (m *MockPermissionClient) ListAll(arg0 context.Context) (*cosmosdb.Permissions, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListAll", arg0)
ret0, _ := ret[0].(*cosmosdb.Permissions)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListAll indicates an expected call of ListAll
func (mr *MockPermissionClientMockRecorder) ListAll(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAll", reflect.TypeOf((*MockPermissionClient)(nil).ListAll), arg0)
}
// Replace mocks base method
func (m *MockPermissionClient) Replace(arg0 context.Context, arg1 *cosmosdb.Permission) (*cosmosdb.Permission, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Replace", arg0, arg1)
ret0, _ := ret[0].(*cosmosdb.Permission)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Replace indicates an expected call of Replace
func (mr *MockPermissionClientMockRecorder) Replace(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Replace", reflect.TypeOf((*MockPermissionClient)(nil).Replace), arg0, arg1)
}

84
pkg/util/oidc/oidc.go Normal file
Просмотреть файл

@ -0,0 +1,84 @@
package oidc
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"encoding/json"
"github.com/coreos/go-oidc"
)
type Verifier interface {
Verify(context.Context, string) (Token, error)
}
type idTokenVerifier struct {
*oidc.IDTokenVerifier
}
func (v *idTokenVerifier) Verify(ctx context.Context, rawIDToken string) (Token, error) {
t, err := v.IDTokenVerifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, err
}
return &token{t}, nil
}
func NewVerifier(ctx context.Context, issuer, clientID string) (Verifier, error) {
provider, err := oidc.NewProvider(ctx, issuer)
if err != nil {
return nil, err
}
return &idTokenVerifier{
provider.Verifier(&oidc.Config{
ClientID: clientID,
}),
}, nil
}
type Token interface {
Claims(interface{}) error
Subject() string
}
type token struct {
t *oidc.IDToken
}
func (t *token) Claims(v interface{}) error {
return t.t.Claims(v)
}
func (t *token) Subject() string {
return t.t.Subject
}
type NoopVerifier struct {
Err error
}
func (v *NoopVerifier) Verify(ctx context.Context, rawtoken string) (Token, error) {
if v.Err != nil {
return nil, v.Err
}
return NoopClaims(rawtoken), nil
}
type NoopClaims []byte
func (c NoopClaims) Claims(v interface{}) error {
return json.Unmarshal(c, v)
}
func (c NoopClaims) Subject() string {
var m map[string]interface{}
_ = json.Unmarshal(c, &m)
subject, _ := m["sub"].(string)
return subject
}

75
vendor/github.com/jim-minter/go-cosmosdb/pkg/gencosmosdb/bindata.go сгенерированный поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

2
vendor/modules.txt поставляемый
Просмотреть файл

@ -551,7 +551,7 @@ github.com/hashicorp/hcl/json/token
github.com/imdario/mergo
# github.com/inconshreveable/mousetrap v1.0.0
github.com/inconshreveable/mousetrap
# github.com/jim-minter/go-cosmosdb v0.0.0-20201119201311-b37af9b82812
# github.com/jim-minter/go-cosmosdb v0.0.0-20210320020825-d7f11ed7bd6d
## explicit
github.com/jim-minter/go-cosmosdb/cmd/gencosmosdb
github.com/jim-minter/go-cosmosdb/pkg/gencosmosdb