worker: Support client_credentials grant type in client

This will allow us to use the service accounts which work against
identity.api.openshift.com. These are much easier to manage, especially
with the new multi-tenancy, as there's a single page to create/expire
them across an account.

They also have the added benefit of not expiring automatically when
they're not used like offline tokens, and immediate expiration when
desired.
This commit is contained in:
Sanne Raymaekers 2022-03-08 15:45:39 +01:00
parent 8900bcec40
commit 2023f7731d
8 changed files with 84 additions and 19 deletions

View file

@ -142,6 +142,8 @@ func main() {
Authentication *struct {
OAuthURL string `toml:"oauth_url"`
OfflineTokenPath string `toml:"offline_token"`
ClientId string `toml:"client_id"`
ClientSecretPath string `toml:"client_secret"`
} `toml:"authentication"`
RelaxTimeoutFactor uint `toml:"RelaxTimeoutFactor"`
BasePath string `toml:"base_path"`
@ -205,7 +207,7 @@ func main() {
BaseURL: address,
BasePath: config.BasePath,
})
} else if config.Authentication != nil && config.Authentication.OfflineTokenPath != "" {
} else if config.Authentication != nil {
var conf *tls.Config
conConf := &connectionConfig{
CACertFile: "/etc/osbuild-composer/ca-crt.pem",
@ -217,13 +219,22 @@ func main() {
}
}
t, err := ioutil.ReadFile(config.Authentication.OfflineTokenPath)
if err != nil {
logrus.Fatalf("Could not read offline token: %v", err)
token := ""
if config.Authentication.OfflineTokenPath != "" {
t, err := ioutil.ReadFile(config.Authentication.OfflineTokenPath)
if err != nil {
logrus.Fatalf("Could not read offline token: %v", err)
}
token = strings.TrimSpace(string(t))
}
token := strings.TrimSpace(string(t))
if token != "" && config.Authentication.OAuthURL == "" {
logrus.Fatal("OAuth URL should be specified together with the offline token")
clientSecret := ""
if config.Authentication.ClientSecretPath != "" {
cs, err := ioutil.ReadFile(config.Authentication.ClientSecretPath)
if err != nil {
logrus.Fatalf("Could not read client secret: %v", err)
}
clientSecret = strings.TrimSpace(string(cs))
}
client, err = worker.NewClient(worker.ClientConfig{
@ -231,6 +242,8 @@ func main() {
TlsConfig: conf,
OfflineToken: token,
OAuthURL: config.Authentication.OAuthURL,
ClientId: config.Authentication.ClientId,
ClientSecret: clientSecret,
BasePath: config.BasePath,
})
if err != nil {

View file

@ -24,6 +24,8 @@ type Client struct {
offlineToken string
oAuthURL string
accessToken string
clientId string
clientSecret string
tokenMu sync.RWMutex
}
@ -33,6 +35,8 @@ type ClientConfig struct {
TlsConfig *tls.Config
OfflineToken string
OAuthURL string
ClientId string
ClientSecret string
BasePath string
}
@ -76,6 +80,15 @@ func NewClient(conf ClientConfig) (*Client, error) {
panic(err)
}
if conf.OAuthURL != "" {
if conf.ClientId == "" {
return nil, fmt.Errorf("OAuthURL token url specified but no client id")
}
if conf.OfflineToken == "" && conf.ClientSecret == "" {
return nil, fmt.Errorf("OAuthURL token url specified but no client secret or offline token")
}
}
requester := &http.Client{}
transport := http.DefaultTransport.(*http.Transport).Clone()
if conf.TlsConfig != nil {
@ -88,6 +101,8 @@ func NewClient(conf ClientConfig) (*Client, error) {
requester: requester,
offlineToken: conf.OfflineToken,
oAuthURL: conf.OAuthURL,
clientId: conf.ClientId,
clientSecret: conf.ClientSecret,
}, nil
}
@ -118,19 +133,21 @@ func NewClientUnix(conf ClientConfig) *Client {
}
}
// Note: Only call this function with Client.tokenMu locked!
func (c *Client) refreshAccessToken() error {
c.tokenMu.Lock()
defer c.tokenMu.Unlock()
if c.offlineToken == "" || c.oAuthURL == "" {
return fmt.Errorf("No offline token or oauth url available")
}
data := url.Values{}
data.Set("grant_type", "refresh_token")
data.Set("client_id", "rhsm-api")
data.Set("refresh_token", c.offlineToken)
if c.offlineToken != "" {
data.Set("grant_type", "refresh_token")
data.Set("client_id", c.clientId)
data.Set("refresh_token", c.offlineToken)
}
if c.clientSecret != "" {
data.Set("grant_type", "client_credentials")
data.Set("client_id", c.clientId)
data.Set("client_secret", c.clientSecret)
}
resp, err := http.PostForm(c.oAuthURL, data)
if err != nil {
@ -159,8 +176,7 @@ func (c *Client) NewRequest(method, url string, headers map[string]string, body
return nil, err
}
// If we're using OAUTH, add the Bearer token
if c.offlineToken != "" {
if c.oAuthURL != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token()))
}
@ -173,7 +189,7 @@ func (c *Client) NewRequest(method, url string, headers map[string]string, body
return nil, err
}
if resp.StatusCode == http.StatusUnauthorized {
if resp.StatusCode == http.StatusUnauthorized && c.oAuthURL != "" {
err = c.refreshAccessToken()
if err != nil {
return nil, err

View file

@ -399,6 +399,7 @@ func TestOAuth(t *testing.T) {
client, err := worker.NewClient(worker.ClientConfig{
BaseURL: proxySrv.URL,
TlsConfig: nil,
ClientId: "rhsm-api",
OfflineToken: offlineToken,
OAuthURL: oauthSrv.URL,
BasePath: "/api/image-builder-worker/v1",

View file

@ -0,0 +1,26 @@
#!/bin/bash
set -eo pipefail
source /tmp/cloud_init_vars
echo "Writing client credentials."
if [[ -z "$CLIENT_CREDENTIALS_ARN" ]]; then
echo "CLIENT_CREDENTIALS_ARN not defined, skipping."
exit 0
fi
# get client credentials
/usr/local/bin/aws secretsmanager get-secret-value \
--endpoint-url "${SECRETS_MANAGER_ENDPOINT_URL}" \
--secret-id "${CLIENT_CREDENTIALS_ARN}" | jq -r ".SecretString" > /tmp/client-credentials.json
CLIENT_ID=$(jq -r ".client_id" /tmp/client-credentials.json)
jq -r ".client_secret" /tmp/client-credentials.json > /etc/osbuild-worker/client-secret
rm -f /tmp/client-credentials.json
sudo tee -a /etc/osbuild-worker/osbuild-worker.toml > /dev/null << EOF
[authentication]
oauth_url = "https://identity.api.openshift.com/auth/realms/rhoas/protocol/openid-connect/token"
client_id = "${CLIENT_ID}"
client_secret = "/etc/osbuild-worker/client-secret"
EOF

View file

@ -1,9 +1,14 @@
#!/bin/bash
set -euo pipefail
set -eo pipefail
source /tmp/cloud_init_vars
echo "Writing offline token."
if [[ -z "$OFFLINE_TOKEN_ARN" ]]; then
echo "OFFLINE_TOKEN_ARN not defined, skipping."
exit 0
fi
# get offline token
/usr/local/bin/aws secretsmanager get-secret-value \
--endpoint-url "${SECRETS_MANAGER_ENDPOINT_URL}" \
@ -15,5 +20,6 @@ rm -f /tmp/offline-token.json
sudo tee -a /etc/osbuild-worker/osbuild-worker.toml > /dev/null << EOF
[authentication]
oauth_url = "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token"
client_id = "rhsm-api"
offline_token = "/etc/osbuild-worker/offline-token"
EOF

View file

@ -10,6 +10,7 @@ ExecStart=touch /etc/worker-first-boot
ExecStart=/usr/local/libexec/worker-initialization-scripts/set_hostname.sh
ExecStart=/usr/local/libexec/worker-initialization-scripts/vector.sh
ExecStart=/usr/local/libexec/worker-initialization-scripts/offline_token.sh
ExecStart=/usr/local/libexec/worker-initialization-scripts/client_credentials.sh
ExecStart=/usr/local/libexec/worker-initialization-scripts/subscription_manager.sh
ExecStart=/usr/local/libexec/worker-initialization-scripts/get_aws_creds.sh
ExecStart=/usr/local/libexec/worker-initialization-scripts/get_azure_creds.sh

View file

@ -1294,6 +1294,7 @@ EOF
cat <<EOF | sudo tee "/etc/osbuild-worker/osbuild-worker.toml"
[authentication]
oauth_url = http://localhost:8081/token
client_id = "rhsm-api"
offline_token = "/etc/osbuild-worker/token"
EOF

View file

@ -90,6 +90,7 @@ EOF
sudo tee "/etc/osbuild-worker/osbuild-worker.toml" >/dev/null <<EOF
[authentication]
oauth_url = "http://localhost:8081/token"
client_id = "rhsm-api"
offline_token = "/etc/osbuild-worker/token"
[koji.localhost.kerberos]