Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve security of database url #313

Merged
merged 15 commits into from
Jul 16, 2024
Merged
4 changes: 3 additions & 1 deletion backend/lib/azimutt/organizations/organization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ defmodule Azimutt.Organizations.Organization do
field :plan_seats, :integer
field :plan_validated, :utc_datetime_usec
field :free_trial_used, :utc_datetime_usec
field :gateway, :string
field :is_personal, :boolean
embeds_one :data, Organization.Data, on_replace: :update
belongs_to :created_by, User, source: :created_by
Expand Down Expand Up @@ -120,7 +121,8 @@ defmodule Azimutt.Organizations.Organization do
:logo,
:description,
:github_username,
:twitter_username
:twitter_username,
:gateway
])
|> put_change(:updated_by_id, current_user.id)
end
Expand Down
5 changes: 4 additions & 1 deletion backend/lib/azimutt_web/templates/layout/root_elm.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
<body>
<%= render "_hello_comment.html" %>
<script>window.env = '<%= Azimutt.config(:environment) %>'</script>
<script>window.gateway_url = '<%= Azimutt.config(:gateway_url) %>'</script>
<% org_id = @conn.params["organization_id"] %>
<% org = org_id && assigns[:current_user] && assigns[:current_user].organizations |> Enum.find(fn o -> o.id == org_id end) %>
<% org_gateway = org && org.plan && Azimutt.limits().gateway_custom[String.to_atom(org.plan)] && org.gateway %>
<script>window.gateway_url = '<%= org_gateway || Azimutt.config(:gateway_url) %>'</script>
<%= if Azimutt.config(:sentry_frontend_dsn) do %><script>window.sentry_frontend_dsn = '<%= Azimutt.config(:sentry_frontend_dsn) %>'</script><% end %>
<script type="text/javascript" src={Routes.static_path(@conn, "/elm/script.js")} />
<%= render "_scripts.html", conn: @conn, current_user: assigns[:current_user] %>
Expand Down
31 changes: 29 additions & 2 deletions backend/lib/azimutt_web/templates/organization/edit.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,45 @@
<div class="sm:col-span-6">
<%= label f, :name, "Organization name", class: "block text-sm font-medium text-gray-700" %>
<div class="mt-1">
<%= text_input f, :name, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
<%= text_input f, :name, class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" %>
<%= error_tag f, :name %>
</div>
</div>
<div class="sm:col-span-6">
<%= label f, :description, class: "block text-sm font-medium text-gray-700" %>
<div class="mt-1">
<%= textarea f, :description, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border border-gray-300 rounded-md" %>
<%= textarea f, :description, class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" %>
<%= error_tag f, :description %>
</div>
<p class="mt-1 text-sm text-gray-500">Describe your organization in a few sentences.</p>
</div>
<%= if Azimutt.limits().gateway_custom[String.to_atom(@organization.plan)] do %>
<div class="sm:col-span-6">
<%= label f, :gateway, "Custom gateway", class: "block text-sm font-medium text-gray-700" %>
<div class="mt-1">
<%= text_input f, :gateway, placeholder: "ex: https://gateway.azimutt.app", class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" %>
<%= error_tag f, :gateway %>
</div>
<p class="mt-1 text-sm text-gray-500">
If local gateway (<a href="http://localhost:4177" target="_blank" class="underline">localhost:4177</a>) is not up (<span class="font-mono">npx azimutt@latest gateway</span>),
Azimutt will reach out to <a href="https://gateway.azimutt.app" target="_blank" class="underline">gateway.azimutt.app</a> or this one if specified.
</p>
</div>
<% else %>
<div class="sm:col-span-6 -m-3 p-3 bg-gray-50 shadow rounded">
<div class="flex justify-between">
<label for="gateway" class="block text-sm font-medium text-gray-700">Custom gateway</label>
<span class="text-sm text-gray-500">Needs <%= AzimuttWeb.LayoutView.plan_badge("enterprise") %></span>
</div>
<div class="mt-1">
<input type="text" name="gateway" id="gateway" disabled placeholder="ex: https://gateway.azimutt.app" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm text-sm leading-6 ring-1 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200">
</div>
<p class="mt-1 text-sm text-gray-500">
If local gateway (<a href="http://localhost:4177" target="_blank" class="underline">localhost:4177</a>) is not up (<span class="font-mono">npx azimutt@latest gateway</span>),
Azimutt will reach out to <a href="https://gateway.azimutt.app" target="_blank" class="underline">gateway.azimutt.app</a> or this one if specified.
</p>
</div>
<% end %>
<div class="mt-3">
<%= submit "Save", class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Azimutt.Repo.Migrations.CustomGateway do
use Ecto.Migration

def change do
alter table(:organizations) do
add :gateway, :string, comment: "custom gateway for the organization"
end
end
end
13 changes: 7 additions & 6 deletions backend/test/azimutt/services/stripe_srv_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ defmodule Azimutt.Services.StripeSrvTest do

@tag :skip
test "get_subscriptions" do
subscriptions = StripeSrv.get_subscriptions("cus_QLWuSJQsP5COgx")
# subscriptions = StripeSrv.get_subscriptions("cus_QLWuSJQsP5COgx")
# IO.inspect(subscriptions, label: "subscriptions")
Comment on lines +15 to 16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid commented-out code in tests.

Commented-out code can lead to confusion and maintenance issues. Consider removing it or replacing it with an actual test.

-      # subscriptions = StripeSrv.get_subscriptions("cus_QLWuSJQsP5COgx")
-      # IO.inspect(subscriptions, label: "subscriptions")
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# subscriptions = StripeSrv.get_subscriptions("cus_QLWuSJQsP5COgx")
# IO.inspect(subscriptions, label: "subscriptions")

end

@tag :skip
test "get_price / get_plan" do
assert StripeSrv.get_plan(StripeSrv.get_price("solo", "monthly")) == {"solo", "monthly"}
assert StripeSrv.get_plan(StripeSrv.get_price("solo", "yearly")) == {"solo", "yearly"}
assert StripeSrv.get_plan(StripeSrv.get_price("team", "monthly")) == {"team", "monthly"}
assert StripeSrv.get_plan(StripeSrv.get_price("team", "yearly")) == {"team", "yearly"}
assert StripeSrv.get_plan(StripeSrv.get_price("pro", "monthly")) == {"pro", "monthly"}
assert StripeSrv.get_plan("", StripeSrv.get_price("solo", "monthly")) == {"solo", "monthly"}
assert StripeSrv.get_plan("", StripeSrv.get_price("solo", "yearly")) == {"solo", "yearly"}
assert StripeSrv.get_plan("", StripeSrv.get_price("team", "monthly")) == {"team", "monthly"}
assert StripeSrv.get_plan("", StripeSrv.get_price("team", "yearly")) == {"team", "yearly"}
assert StripeSrv.get_plan("", StripeSrv.get_price("pro", "monthly")) == {"pro", "monthly"}
assert StripeSrv.get_plan("prod_aaaa", "") == {"enterprise", "yearly"}
end
end
end
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "azimutt",
"version": "0.1.20",
"version": "0.1.21",
"description": "Export database schema from relational or document databases. Import it to https://azimutt.app",
"keywords": [
"database",
Expand Down
8 changes: 4 additions & 4 deletions cli/src/explore.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import open from "open";
import {Logger} from "@azimutt/utils";
import {DatabaseUrlParsed, parseDatabaseUrl} from "@azimutt/models";
import {NodeEnv, startServer, track} from "@azimutt/gateway";
import {buildConfig, NodeEnv, startServer, track} from "@azimutt/gateway";
import {version} from "./version.js";

export async function launchExplore(url: string, instance: string, logger: Logger): Promise<void> {
Expand All @@ -12,13 +12,13 @@ export async function launchExplore(url: string, instance: string, logger: Logge
// const azimuttUrl = `${instance}/embed?database-source=${encodeURIComponent(url)}&mode=full`
// https://azimutt.app/embed?database-source=postgresql://postgres:postgres@localhost/azimutt_dev&mode=full
// const azimuttUrl = `https://azimutt.app/create?database=${encodeURIComponent(url)}&gateway=http://localhost:4177`
await startServer({
await startServer(buildConfig({
NODE_ENV: NodeEnv.production,
LOG_LEVEL: 'info',
API_HOST: 'localhost',
API_PORT: '4177',
CORS_ALLOW_ORIGIN: '*'
})
CORS_ALLOW_ORIGIN: '*',
}))
logger.log(`opening ${azimuttUrl}`)
await open(azimuttUrl)
}
11 changes: 6 additions & 5 deletions cli/src/gateway.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {Logger} from "@azimutt/utils";
import {NodeEnv, startServer, track} from "@azimutt/gateway";
import {buildConfig, NodeEnv, startServer, track} from "@azimutt/gateway";
import {version} from "./version.js";

export async function launchGateway(logger: Logger): Promise<void> {
export async function launchGateway(dataSourceUrls: string, logger: Logger): Promise<void> {
logger.log('Starting Azimutt Gateway...')
track('cli__gateway__start', {version}, 'cli').then(() => {})
await startServer({
await startServer(buildConfig({
NODE_ENV: NodeEnv.production,
LOG_LEVEL: 'info',
API_HOST: 'localhost',
API_PORT: '4177',
CORS_ALLOW_ORIGIN: '*'
})
CORS_ALLOW_ORIGIN: '*',
DATASOURCE_URLS: dataSourceUrls,
}))
}
3 changes: 2 additions & 1 deletion cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ program.name('azimutt')

program.command('gateway')
.description('Launch the gateway server to allow Azimutt to access your local databases.')
.action((args) => exec(launchGateway(logger), args))
.argument('[datasource_urls]', 'database urls to keep inside the gateway, including credentials')
.action((dataSourceUrls, args) => exec(launchGateway(dataSourceUrls, logger), args))

program.command('explore')
.description('Open Azimutt with your database url to see it immediately.')
Expand Down
2 changes: 1 addition & 1 deletion cli/src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const version = '0.1.20' // FIXME: `process.env.npm_package_version` is not available :/
export const version = '0.1.21' // FIXME: `process.env.npm_package_version` is not available :/
36 changes: 19 additions & 17 deletions frontend/src/Components/Organisms/Details.elm
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import Libs.Html.Attributes exposing (ariaHidden, ariaLabel, css, role)
import Libs.List as List
import Libs.Maybe as Maybe
import Libs.Models.Bytes as Bytes
import Libs.Models.DatabaseKind as DatabaseKind
import Libs.Models.HtmlId exposing (HtmlId)
import Libs.Models.Notes exposing (Notes)
import Libs.Models.Tag as Tag exposing (Tag)
Expand Down Expand Up @@ -816,37 +815,40 @@ viewSource openDataExplorer table column rows origin =
SourceKind.DatabaseConnection _ ->
Icon.solid Icons.sources.database "opacity-50 mr-1" |> Tooltip.r "Database source"

SourceKind.SqlLocalFile _ _ _ ->
SourceKind.SqlLocalFile _ ->
Icon.solid Icons.sources.sql "opacity-50 mr-1" |> Tooltip.r "SQL source"

SourceKind.SqlRemoteFile _ _ ->
SourceKind.SqlRemoteFile _ ->
Icon.solid Icons.sources.sql "opacity-50 mr-1" |> Tooltip.r "SQL source"

SourceKind.PrismaLocalFile _ _ _ ->
SourceKind.PrismaLocalFile _ ->
Icon.solid Icons.sources.prisma "opacity-50 mr-1" |> Tooltip.r "Prisma source"

SourceKind.PrismaRemoteFile _ _ ->
SourceKind.PrismaRemoteFile _ ->
Icon.solid Icons.sources.prisma "opacity-50 mr-1" |> Tooltip.r "Prisma source"

SourceKind.JsonLocalFile _ _ _ ->
SourceKind.JsonLocalFile _ ->
Icon.solid Icons.sources.json "opacity-50 mr-1" |> Tooltip.r "JSON source"

SourceKind.JsonRemoteFile _ _ ->
SourceKind.JsonRemoteFile _ ->
Icon.solid Icons.sources.json "opacity-50 mr-1" |> Tooltip.r "JSON source"

SourceKind.AmlEditor ->
Icon.solid Icons.sources.aml "opacity-50 mr-1" |> Tooltip.r "AML source"
, text (origin.name ++ (rows |> Maybe.mapOrElse (\r -> " (" ++ String.fromInt r ++ " rows)") ""))
, origin.db
|> Maybe.andThen DatabaseKind.fromUrl
|> Maybe.map (\kind -> column |> Maybe.mapOrElse (DbQuery.exploreColumn kind table) (DbQuery.exploreTable kind table))
|> Maybe.map
(\query ->
button [ type_ "button", onClick (openDataExplorer origin.id query), class "ml-1" ]
[ Icon.solid Icon.ArrowCircleRight "opacity-50" ]
|> Tooltip.r "Browse data"
)
|> Maybe.withDefault (text "")
, case origin.kind of
SourceKind.DatabaseConnection db ->
let
query : SqlQueryOrigin
query =
column |> Maybe.mapOrElse (DbQuery.exploreColumn db.kind table) (DbQuery.exploreTable db.kind table)
in
button [ type_ "button", onClick (openDataExplorer origin.id query), class "ml-1", ariaLabel "Browse data" ]
[ Icon.solid Icon.ArrowCircleRight "opacity-50" ]
|> Tooltip.r "Browse data"

_ ->
text ""
]


Expand Down
5 changes: 3 additions & 2 deletions frontend/src/Components/Organisms/TableRow.elm
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Libs.Html.Attributes exposing (ariaExpanded, ariaHaspopup, css)
import Libs.Html.Events exposing (PointerEvent, onContextMenu, onDblClick, onPointerUp)
import Libs.List as List
import Libs.Maybe as Maybe
import Libs.Models.DatabaseKind as DatabaseKind
import Libs.Models.DatabaseKind as DatabaseKind exposing (DatabaseKind(..))
import Libs.Models.DateTime as DateTime
import Libs.Models.HtmlId exposing (HtmlId)
import Libs.Models.Notes exposing (Notes)
Expand All @@ -45,6 +45,7 @@ import Models.Project.ColumnName exposing (ColumnName)
import Models.Project.ColumnPath as ColumnPath exposing (ColumnPath, ColumnPathStr)
import Models.Project.ColumnRef exposing (ColumnRef)
import Models.Project.ColumnType exposing (ColumnType)
import Models.Project.DatabaseUrlStorage as DatabaseUrlStorage
import Models.Project.Relation as Relation exposing (Relation)
import Models.Project.RowPrimaryKey as RowPrimaryKey exposing (RowPrimaryKey)
import Models.Project.RowValue exposing (RowValue)
Expand Down Expand Up @@ -985,7 +986,7 @@ docSource : Source
docSource =
{ id = SourceId.one
, name = "azimutt_dev"
, kind = DatabaseConnection "postgresql://postgres:postgres@localhost:5432/azimutt_dev"
, kind = DatabaseConnection { kind = PostgreSQL, url = Just "postgresql://postgres:postgres@localhost:5432/azimutt_dev", storage = DatabaseUrlStorage.Project }
, content = Array.empty
, tables =
[ docTable "public" "users" [ ( "id", "uuid", False ), ( "slug", "varchar", False ), ( "name", "varchar", False ), ( "email", "varchar", False ), ( "provider", "varchar", True ), ( "provider_uid", "varchar", True ), ( "avatar", "varchar", False ), ( "github_username", "varchar", True ), ( "twitter_username", "varchar", True ), ( "is_admin", "boolean", False ), ( "hashed_password", "varchar", True ), ( "last_signin", "timestamp", False ), ( "created_at", "timestamp", False ), ( "updated_at", "timestamp", False ), ( "confirmed_at", "timestamp", True ), ( "deleted_at", "timestamp", True ), ( "data", "json", False ), ( "onboarding", "json", False ), ( "provider_data", "json", True ), ( "tags", "varchar[]", False ) ]
Expand Down
Loading
Loading