123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- <?php
- namespace YdbPlatform\Ydb;
- use DateTime;
- use DateTimeImmutable;
- use Grpc\ChannelCredentials;
- use Psr\Log\LoggerInterface;
- use YdbPlatform\Ydb\Auth\Implement\AccessTokenAuthentication;
- use YdbPlatform\Ydb\Auth\Implement\AnonymousAuthentication;
- use YdbPlatform\Ydb\Auth\Implement\JwtWithJsonAuthentication;
- use YdbPlatform\Ydb\Auth\Implement\JwtWithPrivateKeyAuthentication;
- use YdbPlatform\Ydb\Auth\Implement\MetadataAuthentication;
- use YdbPlatform\Ydb\Auth\Implement\OAuthTokenAuthentication;
- use YdbPlatform\Ydb\Contracts\IamTokenContract;
- use function filter_var;
- class Iam implements IamTokenContract
- {
- use Traits\LoggerTrait;
- const IAM_TOKEN_API_URL = 'https://iam.api.cloud.yandex.net/iam/v1/tokens';
- const METADATA_URL = 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token';
- const DEFAULT_TOKEN_EXPIRES_AT = 2; // hours
- /**
- * @var string
- */
- protected $iam_token;
- /**
- * @var int
- */
- protected $expires_at;
- /**
- * @var array
- */
- protected $config = [];
- /**
- * @var LoggerInterface
- */
- protected $logger;
- /**
- * @var int
- */
- protected $refresh_at;
- /**
- * @param array $config
- * @param LoggerInterface|null $logger
- */
- public function __construct(array $config = [], LoggerInterface $logger = null)
- {
- if ($config)
- {
- $this->config = $this->parseConfig($config);
- }
- $this->logger = $logger;
- $this->initConfig();
- }
- /**
- * @param string $key
- * @param string|null $default
- * @return mixed|null
- */
- public function config($key, $default = null)
- {
- return $this->config[$key] ?? $default;
- }
- /**
- * @param bool $force
- * @return string
- * @throws Exception
- */
- public function token($force = false)
- {
- if ($force || !($token = $this->loadToken()))
- {
- $token = $this->newToken();
- }
- return $token;
- }
- /**
- * @return string|null
- * @throws Exception
- */
- public function newToken()
- {
- $this->logger()->debug('YDB: Obtaining new token...');
- $tokenInfo = $this->config('credentials')->getTokenInfo();
- $this->iam_token = $tokenInfo->getToken();
- $this->expires_at = $tokenInfo->getExpiresAt();
- $this->refresh_at = $tokenInfo->getRefreshAt();
- $this->saveToken((object)[
- "iamToken" => $tokenInfo->getToken(),
- "expiresAt" => $tokenInfo->getExpiresAt(),
- "refreshAt" => $tokenInfo->getRefreshAt()
- ]);
- return $tokenInfo->getToken();
- }
- /**
- * @return ChannelCredentials
- */
- public function getCredentials()
- {
- if ($this->config('insecure'))
- {
- return ChannelCredentials::createInsecure();
- }
- $root_pem_file = $this->config('root_cert_file');
- if ($root_pem_file && is_file($root_pem_file))
- {
- $pem = file_get_contents($root_pem_file);
- }
- return ChannelCredentials::createSsl($pem ?? null);
- }
- /**
- * @param array $config
- * @return array
- */
- protected function parseConfig(array $config)
- {
- $parsedConfig = [];
- $stringParams = [
- 'temp_dir',
- 'root_cert_file',
- 'access_token',
- 'oauth_token',
- 'key_id',
- 'service_account_id',
- 'private_key_file',
- 'service_file',
- ];
- if (isset($config["credentials"])){
- $parsedConfig["credentials"] = $config["credentials"];
- }
- if (isset($config["refresh_token_ratio"])){
- $parsedConfig["refresh_token_ratio"] = $config["refresh_token_ratio"];
- }
- foreach ($stringParams as $param)
- {
- $parsedConfig[$param] = (string)($config[$param] ?? '');
- }
- $boolParams = [
- 'use_metadata',
- 'anonymous',
- 'insecure',
- ];
- foreach ($boolParams as $param)
- {
- $parsedConfig[$param] = (
- isset($config[$param])
- && filter_var($config[$param], \FILTER_VALIDATE_BOOLEAN)
- );
- }
- return $parsedConfig;
- }
- /**
- * @return void
- * @throws Exception
- */
- protected function initConfig()
- {
- if (!$this->config('temp_dir'))
- {
- $this->config['temp_dir'] = sys_get_temp_dir();
- }
- if ($this->config('credentials')){
- $this->logger()->info('YDB: Authentication method: '. $this->config('credentials')->getName());
- }
- else if ($this->config('anonymous'))
- {
- $this->logger()->info('YDB: Authentication method: Anonymous');
- $this->config['credentials'] = new AnonymousAuthentication();
- $this->config['credentials']->setLogger($this->logger());
- }
- else if ($this->config('use_metadata'))
- {
- $this->logger()->info('YDB: Authentication method: Metadata URL');
- $this->config['credentials'] = new MetadataAuthentication();
- $this->config['credentials']->setLogger($this->logger());
- }
- else if ($serviceFile = $this->config('service_file'))
- {
- $this->logger()->info('YDB: Authentication method: SA JSON file');
- if (is_file($serviceFile))
- {
- $this->config['credentials'] = new JwtWithJsonAuthentication($serviceFile);
- $this->config['credentials']->setLogger($this->logger());
- }
- else
- {
- throw new Exception('Service file [' . $serviceFile . '] is missing.');
- }
- }
- else if ($privateKeyFile = $this->config('private_key_file'))
- {
- $this->logger()->info('YDB: Authentication method: Private key');
- if (is_file($privateKeyFile))
- {
- $this->config['credentials'] = new JwtWithPrivateKeyAuthentication($this->config('key_id'),
- $this->config('service_account_id'), $privateKeyFile);
- $this->config['credentials']->setLogger($this->logger());
- }
- else
- {
- throw new Exception('Private key [' . $privateKeyFile . '] is missing.');
- }
- }
- else if ($accessToken = $this->config('access_token')){
- $this->logger()->info('YDB: Authentication method: Access token');
- $this->config['credentials'] = new AccessTokenAuthentication($accessToken);
- $this->config['credentials']->setLogger($this->logger());
- }
- else if ($oauthToken = $this->config('oauth_token'))
- {
- $this->logger()->info('YDB: Authentication method: OAuth token');
- $this->config['credentials'] = new OAuthTokenAuthentication($oauthToken);
- $this->config['credentials']->setLogger($this->logger());
- }
- if ($this->config('credentials') !== null){
- $this->config['credentials']->setRefreshTokenRatio($this->config('refresh_token_ratio', 0.1));
- }
- else
- {
- throw new Exception('No authentication method is used.');
- }
- }
- /**
- * @return string
- */
- protected function getJwtToken()
- {
- $now = new DateTimeImmutable;
- $token = (new Jwt\Jwt($this->config('private_key'), $this->config('key_id')))
- ->issuedBy($this->config('service_account_id'))
- ->issuedAt($now)
- ->expiresAt($now->modify('+1 hour'))
- ->permittedFor(static::IAM_TOKEN_API_URL)
- ->getToken();
- return $token;
- }
- /**
- * @return string|null
- * @throws Exception
- */
- protected function requestTokenFromMetadata()
- {
- $curl = curl_init(static::METADATA_URL);
- curl_setopt_array($curl, [
- CURLOPT_RETURNTRANSFER => 1,
- CURLOPT_SSL_VERIFYPEER => 0,
- CURLOPT_SSL_VERIFYHOST => 0,
- CURLOPT_HEADER => 0,
- CURLOPT_HTTPHEADER => [
- 'Accept: application/json',
- 'Metadata-Flavor:Google',
- ],
- ]);
- $result = curl_exec($curl);
- $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
- if ($status === 200)
- {
- $rawToken = json_decode($result);
- if (isset($rawToken->access_token))
- {
- $token = (object)[
- 'iamToken' => $rawToken->access_token,
- ];
- if (isset($rawToken->expires_in))
- {
- $token->expiresAt = time() + $rawToken->expires_in;
- }
- $this->logger()->info('YDB: Obtained new IAM token from Metadata [...' . substr($token->iamToken, -6) . '].');
- $this->saveToken($token);
- return $token->iamToken;
- }
- else
- {
- $this->logger()->error('YDB: Failed to obtain new IAM token from Metadata', [
- 'status' => $status,
- 'result' => $result,
- ]);
- throw new Exception('Failed to obtain new iamToken from Metadata: no token was received.');
- }
- }
- else
- {
- $this->logger()->error('YDB: Failed to obtain new IAM token from Metadata', [
- 'status' => $status,
- 'result' => $result,
- ]);
- throw new Exception('Failed to obtain new iamToken from Metadata: response status is ' . $status);
- }
- }
- /**
- * @var string
- */
- protected $token_temp_file;
- /**
- * @return string
- */
- protected function getTokenTempFile()
- {
- if (empty($this->token_temp_file))
- {
- $temp_dir = $this->config('temp_dir');
- if (!is_dir($temp_dir))
- {
- mkdir($temp_dir, 0600, true);
- }
- $this->token_temp_file = $temp_dir . '/ydb-iam-' . md5(serialize($this->config)) . '.json';
- }
- return $this->token_temp_file;
- }
- /**
- * @return string|null
- * @throws Exception
- */
- protected function loadToken()
- {
- if ($this->iam_token)
- {
- if ($this->refresh_at <= time()){
- try {
- return $this->newToken();
- } catch (\Exception $e){
- return $this->iam_token;
- }
- }
- return $this->iam_token;
- }
- return $this->loadTokenFromFile();
- }
- /**
- * @return null
- */
- protected function loadTokenFromFile()
- {
- $tokenFile = $this->getTokenTempFile();
- if (is_file($tokenFile))
- {
- $token = json_decode(file_get_contents($tokenFile));
- if (isset($token->iamToken) && $token->expiresAt > time())
- {
- $this->iam_token = $token->iamToken;
- $this->expires_at = $token->expiresAt;
- $this->refresh_at = $token->refreshAt ?? time();
- $this->logger()->debug('YDB: Reused token [...' . substr($this->iam_token, -6) . '].');
- return $token->iamToken;
- }
- }
- return null;
- }
- /**
- * @param object $token
- * @throws Exception
- */
- protected function saveToken($token)
- {
- $tokenFile = $this->getTokenTempFile();
- $this->iam_token = $token->iamToken;
- $this->expires_at = $this->convertExpiresAt($token->expiresAt ?? '');
- $this->refresh_at = $token->refreshAt;
- $randPath = $tokenFile."-tmp".bin2hex(random_bytes(10));
- file_put_contents($randPath, json_encode([
- 'iamToken' => $this->iam_token,
- 'expiresAt' => $this->expires_at,
- 'refreshAt' => $this->refresh_at
- ]));
- rename($randPath, $tokenFile);
- }
- /**
- * @param string $expiresAt
- * @return int
- */
- protected function convertExpiresAt($expiresAt)
- {
- if (is_int($expiresAt))
- {
- return $expiresAt;
- }
- $time = time() + 60 * 60 * static::DEFAULT_TOKEN_EXPIRES_AT;
- if (preg_match('/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d+)?(.*)$/', $expiresAt, $matches))
- {
- $time = new DateTime($matches[1] . $matches[2]);
- $time = (int)$time->format('U');
- }
- return $time;
- }
- }
|