зеркало из https://github.com/Azure/ARO-RP.git
Merge pull request #1422 from jim-minter/dbtoken
DB token service for gateway
This commit is contained in:
Коммит
a3099f14ba
|
@ -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
|
||||
|
|
|
@ -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]",
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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
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
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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче