<turbo-stream action="append" target="posts_list"><template>    <div class="postbit" id="346441" data-post-id="346441">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="chgeuer" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/chgeuer/120/10510_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  chgeuer
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Various Azure services allow you to authenticate using 2 different ways, some sort of password-based authN, and using Entra ID (formerly known as Azure Active Directory).</p>
<p>First a bit of history: When storage accounts were released back in 2008/2009, you only had storage account keys (2 secrets), that you supplied per request. Then shared access signatures came out (where you derive a value from the secrets). Then AAD/Entra authN was introduced. Same for SQL database (and MySQL and PostgreSQL), where in early versions you had to pick an admin username and password, and later supported Entra authentication.</p>
<p>Now storing passwords for services is kind-of a bad thing: You need a different password/credential for each service you want to call. If your app calls storage, SQL, service bus, event hub, Key Vault, you have to store (and protect) 5 credentials. And if one of these credentials is compromised, you must rotate it for all callers. It’s in general messy.</p>
<p>If you on the other hand have an Entra credential (which still can be a username/password combo like in the example with client_id/client_secret), you still must protect that one. But if it’s compromised, you only rotate a single credential of a single app, all server-side ACLs remain as they are.</p>
<p>What’s even more important, if your app workload runs in the cloud, you can use “managed identity”, where you tie an Entra identity to the VM or web app or Kubernetes pod you’re running on, and your workload doesn’t have to store any cred at all. When you need a token, you just issue a token request to 169.254.169.254, say you need a token for PostgreSQL, and off you go.</p>
<p><strong>If you don’t want to do Entra authN, just skip all the Entra bits in the blog article and just do your username/password thing like before…</strong></p>
<p>However, I strongly recommend to use Entra authN. For example, in the words of the storage team:</p>
<blockquote>
<p>For optimal security, Microsoft recommends using Microsoft Entra ID with managed identities to authorize requests against blob, queue, and table data, whenever possible. Authorization with Microsoft Entra ID and managed identities provides superior security and ease of use over Shared Key authorization. <a href="https://learn.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage?tabs=azure-portal" rel="noopener nofollow ugc">src</a></p>
</blockquote>
<p>Using Entra authentication at the first glance makes it more complex: You must create an Entra app (setup takes longer), and prior authenticating to the DB you need to request a token. However, once you figure out you must rotate creds, or one credential is compromized, our you want to audit which app did what, you certainly start to appreciate having not used the same PostgreSQL password for all your workloads.</p>
<p>Added a short section here:</p><aside class="onebox allowlistedgeneric" data-onebox-src="https://cookbook.geuer-pollmann.de/azure/postgrex-on-azure#how-to-authenticate-to-postgresql">
  <header class="source">
      <img src="https://cookbook.geuer-pollmann.de/~gitbook/icon?size=small&amp;theme=light" class="site-icon" alt="" width="48" height="48">

      <a href="https://cookbook.geuer-pollmann.de/azure/postgrex-on-azure#how-to-authenticate-to-postgresql" target="_blank" rel="noopener nofollow ugc">cookbook.geuer-pollmann.de</a>
  </header>

  <article class="onebox-body">
    <div class="aspect-image" style="--aspect-ratio:690/362;"><img src="https://cookbook.geuer-pollmann.de/~gitbook/ogimage/BXWDkAymv3UBy3AVVNbd" class="thumbnail" alt="" width="690" height="362"></div>

<h3><a href="https://cookbook.geuer-pollmann.de/azure/postgrex-on-azure#how-to-authenticate-to-postgresql" target="_blank" rel="noopener nofollow ugc">How to authenticate to PostgreSQL? - Postgrex on Azure - Connecting to Azure...</a></h3>

  <p>Written 2024-NOV-14 by Christian Geuer-Pollmann</p>


  </article>

  <div class="onebox-metadata">
    
    
  </div>

  <div style="clear: both"></div>
</aside>
 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="346441" data-batch-url="/posts/batch_likers">
                        6
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/anyone-have-any-experience-with-elixir-on-microsoft-azure/16537/12">Post #11</a>
	                </div>
	            </div>
              <div id="likers-container-346441" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="346441"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #11"></div>
  </section>
</div>
    <div class="postbit" id="354873" data-post-id="354873">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="josevalim" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/josevalim/120/1787_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  josevalim
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>Creator of Elixir</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Fantastic article that just came in handy, thanks for sharing!</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="354873" data-batch-url="/posts/batch_likers">
                        2
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/anyone-have-any-experience-with-elixir-on-microsoft-azure/16537/13">Post #12</a>
	                </div>
	            </div>
              <div id="likers-container-354873" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="354873"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #12"></div>
  </section>
</div>
    <div class="postbit" id="381470" data-post-id="381470">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="jan-mb-me" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/jan-mb-me/120/34448_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  jan-mb-me
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Thank you for sharing this, <a class="mention" href="/u/chgeuer" rel="nofollow">@chgeuer</a>.</p>
<p>I assume the access tokens have an <code>“expires_at”</code> timestamp or the like.<br>
Even if that’s not the case, the “usual” way (f.i. as generated with <code>mix phx.new</code>) to configure the database connection is pretty static via <code>config :my_app, MyApp.Repo,...</code>, i.e. <code>Postgrex.start_link</code> is invoked “behind the scenes”.</p>
<p>Could you point me to any resources about how to implement this more dynamic configuration?</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="381470" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/anyone-have-any-experience-with-elixir-on-microsoft-azure/16537/14">Post #13</a>
	                </div>
	            </div>
              <div id="likers-container-381470" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="381470"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #13"></div>
  </section>
</div>
    <div class="postbit" id="382511" data-post-id="382511">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="chgeuer" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/chgeuer/120/10510_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  chgeuer
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Hey <a class="mention" href="/u/jan-mb-me" rel="nofollow">@jan-mb-me</a> you’re right, EntraID does the standard OAuth2 issuance process and you can see the expires_at attribute. However AFAIK the PostgreSQL wire protocol has no way to re-authenticate or refresh a credential on an already established connection. So if your JWT token expires, you would need to re-establish the TCP connection to PostgreSQL with a fresh token. You might be able to do something like this (haven’t tried myself yet)<img src="https://forum.elixirforum.com/images/emoji/apple/slight_smile.png?v=15" title=":slight_smile:" class="emoji" alt=":slight_smile:" loading="lazy" width="20" height="20"></p>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule MyApp.RepoConfig do
  def configure(opts) do
    token = MyApp.TokenManager.get_valid_token()
    
    # Inject the fresh token as the password
    opts
    |&gt; Keyword.put(:password, token)
    # Ensure SSL is still configured
    |&gt; Keyword.put(:ssl_opts, MyApp.AzureCerts.ssl_opts(opts[:hostname]))
  end
end
</code></pre>
<p>and configure your repo to use that function</p>
<pre data-code-wrap="elixir"><code class="lang-elixir"># config/runtime.exs

config :my_app, MyApp.Repo,
  hostname: "my-db.postgres.database.azure.com",
  username: "my-app-identity",
  # The magic line:
  configure: &amp;MyApp.RepoConfig.configure/1,
  # Standard settings
  pool_size: 10,
  ssl: true
</code></pre> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="382511" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/anyone-have-any-experience-with-elixir-on-microsoft-azure/16537/15">Post #14</a>
	                </div>
	            </div>
              <div id="likers-container-382511" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="382511"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #14"></div>
  </section>
</div>
    <div class="postbit" id="382552" data-post-id="382552">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="chgeuer" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/chgeuer/120/10510_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  chgeuer
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>This works like a charm:</p>
<h1><a name="p-382552-postgresql-1" class="anchor" href="#p-382552-postgresql-1" aria-label="Heading link" rel="nofollow"></a>PostgreSQL</h1>
<pre data-code-wrap="elixir"><code class="lang-elixir">Mix.install(
  [
    {:x509, "~&gt; 0.9.2"},
    {:postgrex, "~&gt; 0.22.0"},
    {:ecto_sql, "~&gt; 3.12"},
    {:req, "~&gt; 0.5.17"},
    {:envious, "~&gt; 1.4"},
    {:kino, "~&gt; 0.18.0"}
  ]
)
</code></pre>
<h2><a name="p-382552-section-2" class="anchor" href="#p-382552-section-2" aria-label="Heading link" rel="nofollow"></a>Section</h2>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule MicrosoftCerts do
  defmodule CompileHelpers do
    defp http_get(url) do
      %Req.Response{status: 200, body: body} = Req.get!(url: url)
      body
    end
  
    def download_certs_for_pinning() do
      # https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-networking-ssl-tls#download-root-ca-certificates-and-update-application-clients-in-certificate-pinning-scenarios
      """
      https://www.microsoft.com/pkiops/certs/microsoft%20azure%20rsa%20tls%20issuing%20ca%2004%20-%20xsign.crt
      https://www.microsoft.com/pkiops/certs/Microsoft%20RSA%20Root%20Certificate%20Authority%202017.crt
      https://cacerts.digicert.com/DigiCertGlobalRootG2.crt.pem
      https://dl.cacerts.digicert.com/DigiCertGlobalRootG2.crt.pem
      https://cacerts.digicert.com/DigiCertGlobalRootCA.crt
      """
      |&gt; String.split(["\n"], trim: true)
      |&gt; Enum.map(fn url -&gt;
        {url, http_get(url)}
      end)
      |&gt; Enum.map(fn {url, data} -&gt; 
        cond do
          url |&gt; String.ends_with?(".crt") -&gt; 
            data 
          url |&gt; String.ends_with?(".pem") -&gt; 
            data
            |&gt; X509.Certificate.from_pem!()
            |&gt; X509.Certificate.to_der()
        end
      end)      
      |&gt; Enum.uniq()
    end
  end

  @certs MicrosoftCerts.CompileHelpers.download_certs_for_pinning()

  def ssl_opts(hostname) do
    [
      protocol: :tls,
      protocol_version: :"tlsv1.3",
      verify: :verify_peer,
      cacerts: @certs,
      server_name_indication: String.to_charlist(hostname),
      depth: 3
    ]
  end
end
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">Path.join(System.get_env("HOME"), ".azure_creds.sh")
|&gt; File.read!()
|&gt; Envious.parse!(interpolate: true)
|&gt; System.put_env()
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule MyApp.Repo do
  use Ecto.Repo,
    otp_app: :my_app,
    adapter: Ecto.Adapters.Postgres

  def configure_entra_token(tenant_id, client_id, client_secret) do
    fn opts -&gt;
      {:ok, %Req.Response{status: 200, body: %{"token_type" =&gt; "Bearer", "access_token" =&gt; access_token}}} = 
        Req.request(
          method: :post,
          url: "https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
          path_params_style: :curly,
          path_params: [
          	tenant_id: tenant_id
          ],
          form: [
            grant_type: "client_credentials",
            client_id: client_id,
            client_secret: client_secret,
            scope: "https://ossrdbms-aad.database.windows.net/.default"
          ]
        )
  
      opts
      |&gt; Keyword.put(:password, access_token)
      |&gt; Keyword.put(:ssl, MicrosoftCerts.ssl_opts(opts[:hostname]))
    end
  end
end
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">Application.put_env(:my_app, MyApp.Repo,
  hostname: System.get_env("POSTGRESQL_SERVER_DOMAIN"),
  username: System.get_env("POSTGRESQL_ADMIN_ENTRA_APP_NAME"),
  database: "postgres",
  configure: MyApp.Repo.configure_entra_token(
    System.get_env("POSTGRESQL_ADMIN_ENTRA_TENANT_ID"), 
    System.get_env("POSTGRESQL_ADMIN_ENTRA_CLIENT_ID"),
    System.get_env("POSTGRESQL_ADMIN_ENTRA_CLIENT_SECRET")),
  pool_size: 10,
  ssl: true
)
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">{:ok, repo_pid} = MyApp.Repo.start_link()
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule MyApp.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :username, :string
    field :email, :string
    field :password_hash, :string

    timestamps(type: :naive_datetime)
  end

  def changeset(user, attrs) do
    user
    |&gt; cast(attrs, [:username, :email, :password_hash])
    |&gt; validate_required([:username, :email, :password_hash])
    |&gt; unique_constraint(:username)
    |&gt; unique_constraint(:email)
  end
end
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule MyApp.Migrations.CreateUsers do
  use Ecto.Migration

  def up do
    create_if_not_exists table(:users) do
      add :username, :string, size: 50, null: false
      add :email, :string, size: 255, null: false
      add :password_hash, :string, size: 255, null: false

      timestamps(type: :naive_datetime, default: fragment("CURRENT_TIMESTAMP"))
    end

    create_if_not_exists unique_index(:users, [:username])
    create_if_not_exists unique_index(:users, [:email])
  end

  def down do
    drop_if_exists table(:users)
  end
end
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir"># Run the migration
Ecto.Migrator.up(MyApp.Repo, 1, MyApp.Migrations.CreateUsers)
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir"># To rollback:
# Ecto.Migrator.down(MyApp.Repo, 1, MyApp.Migrations.CreateUsers)
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">import Ecto.Query

# Insert a user
{:ok, user} = 
  %MyApp.User{}
  |&gt; MyApp.User.changeset(%{
    username: "testuser",
    email: "test@example.com",
    password_hash: "hashed_password_here"
  })
  |&gt; MyApp.Repo.insert()
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir"># Query all users
MyApp.Repo.all(MyApp.User)
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir"># Query with conditions
MyApp.Repo.all(from u in MyApp.User, where: u.username == "testuser")
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir"># Get by id
MyApp.Repo.get(MyApp.User, 1)
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir"># Delete all test users
MyApp.Repo.delete_all(MyApp.User)
</code></pre>
<hr>
<h2><a name="p-382552-postgrex-3" class="anchor" href="#p-382552-postgrex-3" aria-label="Heading link" rel="nofollow"></a>Postgrex</h2>
<pre data-code-wrap="elixir"><code class="lang-elixir">create_table_query = 
  """
  CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
  );
  """

drop_table_query =
  """
  DROP TABLE users
  """

db_host = System.get_env("POSTGRESQL_SERVER_DOMAIN")

Kino.nothing()
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">{:ok, %Req.Response{status: 200, body: %{"token_type" =&gt; "Bearer", "access_token" =&gt; access_token}}} = 
  Req.request(
    method: :post,
    url: "https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
    path_params_style: :curly,
    path_params: [
    	tenant_id: System.get_env("POSTGRESQL_ADMIN_ENTRA_TENANT_ID")
    ],
    form: [
      grant_type: "client_credentials",
      client_id: System.get_env("POSTGRESQL_ADMIN_ENTRA_CLIENT_ID"),
      client_secret: System.get_env("POSTGRESQL_ADMIN_ENTRA_CLIENT_SECRET"),
      scope: "https://ossrdbms-aad.database.windows.net/.default"
    ]
  )

{:ok, entra_conn} = Postgrex.start_link(
  hostname: db_host,
  port: 5432, 
  database: "postgres",
  ssl: MicrosoftCerts.ssl_opts(db_host),  
  # The username here is the friendly name of our app...
  username: System.get_env("POSTGRESQL_ADMIN_ENTRA_APP_NAME"),
  password: access_token
)
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">Postgrex.query!(entra_conn, create_table_query, [])
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">Postgrex.query!(entra_conn, drop_table_query, [])
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">{:ok, password_conn} = Postgrex.start_link(
  hostname: db_host,
  port: 5432, 
  database: "postgres",
  ssl: MicrosoftCerts.ssl_opts(db_host),  
  username: System.get_env("POSTGRESQL_ADMIN_NAME"), 
  password: System.get_env("POSTGRESQL_ADMIN_PASSWORD")
)
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">Postgrex.query!(password_conn, create_table_query, [])
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir">Postgrex.query!(password_conn, drop_table_query, [])
</code></pre>
<ul>
<li></li>
</ul> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="382552" data-batch-url="/posts/batch_likers">
                        2
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/anyone-have-any-experience-with-elixir-on-microsoft-azure/16537/16">Post #15</a>
	                </div>
	            </div>
              <div id="likers-container-382552" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="382552"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-last-post cat-last-post" title="Last post!"></div>
  </section>
</div>
</template></turbo-stream><turbo-stream action="replace" target="load-more-container"><template><div id="load-more-container" class="load-more-container">
    <span class="all-loaded">— All posts loaded —</span>
</div></template></turbo-stream>