Skip to content

Commit

Permalink
Add oci join client helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
atburke committed Jan 29, 2025
1 parent 3323789 commit 00ac989
Show file tree
Hide file tree
Showing 5 changed files with 597 additions and 1 deletion.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ require (
software.sslmate.com/src/go-pkcs12 v0.5.0
)

require github.com/oracle/oci-go-sdk/v65 v65.81.0

require (
cel.dev/expr v0.19.1 // indirect
cloud.google.com/go v0.117.0 // indirect
Expand Down Expand Up @@ -506,6 +508,7 @@ require (
github.com/sigstore/rekor v1.3.6 // indirect
github.com/sigstore/timestamp-authority v1.2.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sony/gobreaker v0.5.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,7 @@ github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
Expand Down Expand Up @@ -1960,6 +1961,8 @@ github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsr
github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/oracle/oci-go-sdk/v65 v65.81.0 h1:uyAdy7N7q3cj090zrLCCL+IbL3JHd4IXZi2N5epXeAk=
github.com/oracle/oci-go-sdk/v65 v65.81.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
github.com/parquet-go/parquet-go v0.24.0 h1:VrsifmLPDnas8zpoHmYiWDZ1YHzLmc7NmNwPGkI2JM4=
github.com/parquet-go/parquet-go v0.24.0/go.mod h1:OqBBRGBl7+llplCvDMql8dEKaDqjaFA/VAPw+OJiNiw=
github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0=
Expand Down Expand Up @@ -2157,6 +2160,8 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EE
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/snowflakedb/gosnowflake v1.12.1 h1:IpYK9Wr1dYwPiMSG9RNudAJV0rI0ZOgcNEMXOUiPFX8=
github.com/snowflakedb/gosnowflake v1.12.1/go.mod h1:SYLNMBZ4LXTJfTfJt+M4N40DwabGUx3gkH7VT8hu3Rw=
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
Expand Down
19 changes: 18 additions & 1 deletion lib/auth/join/join.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (

"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/oracle/oci-go-sdk/v65/common/auth"
"go.opentelemetry.io/otel"
"golang.org/x/crypto/ssh"

Expand All @@ -40,6 +41,7 @@ import (
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/join/iam"
"github.com/gravitational/teleport/lib/auth/join/oracle"
"github.com/gravitational/teleport/lib/auth/state"
"github.com/gravitational/teleport/lib/bitbucket"
"github.com/gravitational/teleport/lib/circleci"
Expand Down Expand Up @@ -814,7 +816,22 @@ func registerUsingTPMMethod(
func registerUsingOracleMethod(
ctx context.Context, client joinServiceClient, token string, hostKeys *newHostKeys, params RegisterParams,
) (*proto.Certs, error) {
return nil, trace.NotImplemented("Not implemented")
certs, err := client.RegisterUsingOracleMethod(ctx, func(challenge string) (*proto.RegisterUsingOracleMethodRequest, error) {
provider, err := auth.InstancePrincipalConfigurationProvider()
if err != nil {
return nil, trace.Wrap(err)
}
innerHeaders, outerHeaders, err := oracle.CreateSignedRequest(provider, challenge)
if err != nil {
return nil, trace.Wrap(err)
}
return &proto.RegisterUsingOracleMethodRequest{
RegisterUsingTokenRequest: registerUsingTokenRequestForParams(token, hostKeys, params),
Headers: mapFromHeader(outerHeaders),
PayloadHeaders: mapFromHeader(innerHeaders),
}, nil
})
return certs, trace.Wrap(err)
}

// readCA will read in CA that will be used to validate the certificate that
Expand Down
297 changes: 297 additions & 0 deletions lib/auth/join/oracle/oracle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
// Teleport
// Copyright (C) 2024 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package oracle

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/gravitational/trace"
"github.com/oracle/oci-go-sdk/v65/common"

"github.com/gravitational/teleport/api"
"github.com/gravitational/teleport/lib/defaults"
)

const teleportUserAgent = "teleport/" + api.Version

const (
// DateHeader is the header containing the date to send to Oracle.
DateHeader = "x-date"
// ChallengeHeader is the header containing the Teleport-generated challenge
// string to send to Oracle.
ChallengeHeader = "x-teleport-challenge"
)

const (
tenancyClaim = "opc-tenant"
compartmentClaim = "opc-compartment"
instanceClaim = "opc-instance"
)

type authenticateClientDetails struct {
RequestHeaders http.Header `json:"requestHeaders"`
}

type authenticateClientRequest struct {
Date string `contributesTo:"header" name:"x-date"`
Challenge string `contributesTo:"header" name:"x-teleport-challenge"`
UserAgent string `contributesTo:"header" name:"User-Agent"`
Details authenticateClientDetails `contributesTo:"body"`
}

// Claims are the claims returned by the authenticateClient endpoint.
type Claims struct {
// TenancyID is the ID of the instance's tenant.
TenancyID string `json:"tenant_id"`
// CompartmentID is the ID of the instance's compartment.
CompartmentID string `json:"compartment_id"`
// InstanceID is the instance's ID.
InstanceID string `json:"-"`
}

// Region extracts the region from an instance's claims.
func (c Claims) Region() string {
region, err := ParseRegionFromOCID(c.InstanceID)
if err != nil {
return ""
}
return region
}

type claim struct {
Key string `json:"key"`
Value string `json:"value"`
}

type principal struct {
Claims []claim `json:"claims"`
}

func (p principal) getClaims() Claims {
claims := Claims{}
for _, claim := range p.Claims {
switch claim.Key {
case tenancyClaim:
claims.TenancyID = claim.Value
case compartmentClaim:
claims.CompartmentID = claim.Value
case instanceClaim:
claims.InstanceID = claim.Value
}
}
return claims
}

type authenticateClientResult struct {
ErrorMessage string `json:"errorMessage,omitempty"`
Principal principal `json:"principal,omitempty"`
}

type authenticateClientResponse struct {
Result authenticateClientResult `presentIn:"body"`
}

func newAuthenticateClientRequest(time time.Time, challenge string, headers http.Header) authenticateClientRequest {
req := authenticateClientRequest{
Date: time.UTC().Format(http.TimeFormat),
Challenge: challenge,
UserAgent: teleportUserAgent,
Details: authenticateClientDetails{
RequestHeaders: headers,
},
}
if len(headers) == 0 {
req.Details.RequestHeaders = http.Header{}
}
return req
}

func createAuthHTTPRequest(region string, auth authenticateClientRequest) (*http.Request, error) {
req, err := common.MakeDefaultHTTPRequestWithTaggedStruct(
http.MethodPost,
"",
auth,
)
if err != nil {
return nil, trace.Wrap(err)
}
endpointURL, err := url.Parse(fmt.Sprintf("https://auth.%s.oraclecloud.com/v1/authentication/authenticateClient", region))
if err != nil {
return nil, trace.Wrap(err)
}
// Manually set the host header so it will be sent as part of the grpc.
req.Header.Set("Host", endpointURL.Host)
req.Host = endpointURL.Host
req.URL = endpointURL

// If no headers were provided, this is the inner header payload and we need
// to explicitly include (request-target).
if len(auth.Details.RequestHeaders) == 0 {
req.Header.Set("(request-target)", strings.ToLower(http.MethodPost)+" "+endpointURL.RequestURI())
}
return &req, nil
}

// CreateSignedRequest creates a signed HTTP request to
// https://auth.<region>.oraclecloud.com/v1/authentication/authenticateClient.
// The returned headers should be sent to an auth server as part of
// RegisterUsingOracleMethod.
func CreateSignedRequest(provider common.ConfigurationProvider, challenge string) (innerHeaders, outerHeaders http.Header, err error) {
signedHeaders := append(common.DefaultGenericHeaders(), DateHeader, ChallengeHeader)
signer := common.RequestSigner(provider, signedHeaders, common.DefaultBodyHeaders())
region, err := provider.Region()
if err != nil {
return nil, nil, trace.Wrap(err)
}
now := time.Now().UTC()
innerReq, err := createAuthHTTPRequest(region, newAuthenticateClientRequest(now, challenge, nil))
if err != nil {
return nil, nil, trace.Wrap(err)
}
signer.Sign(innerReq)

outerReq, err := createAuthHTTPRequest(region, newAuthenticateClientRequest(now, challenge, innerReq.Header))
if err != nil {
return nil, nil, trace.Wrap(err)
}
signer.Sign(outerReq)
return innerReq.Header, outerReq.Header, nil
}

// GetAuthorizationHeaderValues gets the key-value pairs encoded in the
// Authorization header as described in the [Oracle API docs].
//
// [Oracle API docs]: https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm#five
func GetAuthorizationHeaderValues(header http.Header) map[string]string {
rawValues := strings.TrimPrefix(header.Get("Authorization"), "Signature ")
keyValuePairs := strings.Split(rawValues, ",")
values := make(map[string]string, len(keyValuePairs))
for _, pair := range keyValuePairs {
k, v, _ := strings.Cut(pair, "=")
values[k] = strings.Trim(v, "\"")
}
return values
}

// CreateRequestFromHeaders recreates an HTTP request to the authenticateClient
// endpoint from its inner and outer headers.
func CreateRequestFromHeaders(region string, innerHeaders, outerHeaders http.Header) (*http.Request, error) {
req, err := createAuthHTTPRequest(region, authenticateClientRequest{
Date: outerHeaders.Get(DateHeader),
Challenge: outerHeaders.Get(ChallengeHeader),
UserAgent: teleportUserAgent,
Details: authenticateClientDetails{
RequestHeaders: innerHeaders,
},
})
if err != nil {
return nil, trace.Wrap(err)
}
req.Header = outerHeaders
return req, nil
}

// FetchOraclePrincipalClaims executes a request to authenticateClient and parses
// the response.
func FetchOraclePrincipalClaims(ctx context.Context, req *http.Request) (Claims, error) {
client, err := defaults.HTTPClient()
if err != nil {
return Claims{}, trace.Wrap(err)
}
// Block redirects.
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}

authResp, err := client.Do(req.WithContext(ctx))
if err != nil {
return Claims{}, trace.Wrap(err)
}
defer authResp.Body.Close()
var resp authenticateClientResponse
unmarshalErr := common.UnmarshalResponse(authResp, &resp)
if authResp.StatusCode >= 300 || resp.Result.ErrorMessage != "" {
msg := resp.Result.ErrorMessage
if msg == "" {
msg = authResp.Status
}
return Claims{}, trace.AccessDenied("%v", msg)
}
if unmarshalErr != nil {
return Claims{}, trace.Wrap(unmarshalErr)
}
return resp.Result.Principal.getClaims(), nil
}

// Hack: StringToRegion will lazily load regions from a config file if its
// input isn't in its hard-coded list, in a non-threadsafe way. Call it here
// to load the config ahead of time so future calls are threadsafe.
var _ = common.StringToRegion("")

// ParseRegion parses a string into a full (not abbreviated) Oracle Cloud
// region. It returns the empty string if the input is not a valid region.
func ParseRegion(rawRegion string) string {
region := common.StringToRegion(rawRegion)
if _, err := region.RealmID(); err != nil {
return ""
}
return string(region)
}

// ParseRegionFromOCID parses an Oracle OCID and returns the embedded region.
// It returns an error if the input is not a valid OCID.
func ParseRegionFromOCID(ocid string) (string, error) {
// OCID format: ocid1.<RESOURCE TYPE>.<REALM>.[REGION][.FUTURE USE].<UNIQUE ID>
// Check format.
ocidParts := strings.Split(ocid, ".")
switch len(ocidParts) {
case 5, 6:
default:
return "", trace.BadParameter("not an ocid")
}
// Check version.
if ocidParts[0] != "ocid1" {
return "", trace.BadParameter("invalid ocid version: %v", ocidParts[0])
}
resourceType := ocidParts[1]
region := ParseRegion(ocidParts[3])
// Check type. Only instance OCIDs should have a region.
switch resourceType {
case "instance":
if region == "" {
return "", trace.BadParameter("invalid region: %v", region)
}
case "compartment", "tenancy":
if ocidParts[3] != "" {
return "", trace.BadParameter("resource type %v should not have a region", resourceType)
}
default:
return "", trace.BadParameter("unsupported resource type: %v", resourceType)
}
// Check realm.
switch ocidParts[2] {
case "oc1", "oc2", "oc3":
default:
return "", trace.BadParameter("invalid realm: %v", ocidParts[2])
}
return region, nil
}
Loading

0 comments on commit 00ac989

Please sign in to comment.