Skip to content

Commit 73121c5

Browse files
committed
chore(guard): refresh protos and implement service account service
1 parent ce6e12f commit 73121c5

21 files changed

+3475
-12
lines changed

guard/lib/guard/application.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ defmodule Guard.Application do
184184
worker: Guard.GrpcServers.UserServer,
185185
active: System.get_env("START_GRPC_USER_API") == "true"
186186
},
187+
%{
188+
worker: Guard.GrpcServers.ServiceAccountServer,
189+
active: System.get_env("START_GRPC_SERVICE_ACCOUNT_API") == "true"
190+
},
187191
%{
188192
worker: Guard.GrpcServers.InstanceConfigServer,
189193
active: System.get_env("START_GRPC_INSTANCE_CONFIG_API") == "true"

guard/lib/guard/front_repo/favorite.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ defmodule Guard.FrontRepo.Favorite do
2727
favorite
2828
|> cast(attrs, [:user_id, :favorite_id, :kind, :organization_id])
2929
|> validate_required([:user_id, :favorite_id, :kind, :organization_id])
30-
|> unique_constraint([:user_id, :organization_id, :favorite_id, :kind], name: :favorites_index)
30+
|> unique_constraint([:user_id, :organization_id, :favorite_id, :kind],
31+
name: :favorites_index
32+
)
3133
end
3234

3335
@spec create_favorite(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
defmodule Guard.FrontRepo.ServiceAccount do
2+
use Ecto.Schema
3+
import Ecto.Changeset
4+
5+
@primary_key {:id, :binary_id, autogenerate: false}
6+
@foreign_key_type :binary_id
7+
8+
@type t :: %__MODULE__{
9+
id: String.t(),
10+
description: String.t(),
11+
creator_id: String.t(),
12+
user: Guard.FrontRepo.User.t() | nil,
13+
created_at: DateTime.t(),
14+
updated_at: DateTime.t()
15+
}
16+
17+
schema "service_accounts" do
18+
field(:description, :string)
19+
field(:creator_id, :binary_id)
20+
21+
# The id field itself is the foreign key to user
22+
belongs_to(:user, Guard.FrontRepo.User, foreign_key: :id, define_field: false)
23+
24+
timestamps(inserted_at: :created_at, updated_at: :updated_at, type: :utc_datetime)
25+
end
26+
27+
@doc """
28+
Changeset for creating a new service account.
29+
"""
30+
def changeset(service_account, attrs) do
31+
service_account
32+
|> cast(attrs, [:description, :id, :creator_id])
33+
|> validate_required([:id, :creator_id])
34+
|> validate_length(:description,
35+
max: 500,
36+
message: "Description cannot exceed 500 characters"
37+
)
38+
|> foreign_key_constraint(:id, name: :service_accounts_id_fkey)
39+
|> foreign_key_constraint(:creator_id, name: :service_accounts_creator_id_fkey)
40+
end
41+
42+
@doc """
43+
Changeset for updating an existing service account.
44+
Only allows updating the description field.
45+
"""
46+
def update_changeset(service_account, attrs) do
47+
service_account
48+
|> cast(attrs, [:description])
49+
|> validate_length(:description,
50+
max: 500,
51+
message: "Description cannot exceed 500 characters"
52+
)
53+
end
54+
end

guard/lib/guard/front_repo/user.ex

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ defmodule Guard.FrontRepo.User do
4141
field(:remember_created_at, :utc_datetime)
4242
field(:visited_at, :utc_datetime)
4343

44-
field(:creation_source, Ecto.Enum, values: [:okta, :saml_jit])
44+
field(:creation_source, Ecto.Enum, values: [:okta, :saml_jit, :service_account])
4545
field(:single_org_user, :boolean)
4646
field(:org_id, :binary_id)
4747
field(:idempotency_token, :string)
@@ -53,6 +53,9 @@ defmodule Guard.FrontRepo.User do
5353
field(:deactivated, :boolean)
5454
field(:deactivated_at, :utc_datetime)
5555

56+
# Service account relationship
57+
has_one(:service_account, Guard.FrontRepo.ServiceAccount, foreign_key: :id)
58+
5659
timestamps(inserted_at: :created_at, updated_at: :updated_at, type: :utc_datetime)
5760
end
5861

@@ -229,4 +232,58 @@ defmodule Guard.FrontRepo.User do
229232

230233
invalid_token_string or exists_by_token
231234
end
235+
236+
@doc """
237+
Returns true if the user is a service account.
238+
"""
239+
def service_account?(%__MODULE__{creation_source: :service_account}), do: true
240+
def service_account?(_user), do: false
241+
242+
@doc """
243+
Changeset for creating a service account user.
244+
This validates the specific requirements for service account users.
245+
"""
246+
def service_account_changeset(user, params) do
247+
user
248+
|> cast(params, [
249+
:email,
250+
:name,
251+
:company,
252+
:authentication_token,
253+
:salt,
254+
:creation_source,
255+
:single_org_user,
256+
:org_id,
257+
:idempotency_token
258+
])
259+
|> validate_required([:email, :name, :creation_source, :org_id])
260+
|> validate_inclusion(:creation_source, [:service_account])
261+
|> put_change(:single_org_user, true)
262+
|> validate_format(:email, ~r/^[\w\-\.]+@sa\.[\w\-\.]+\.semaphoreci\.com$/i,
263+
message:
264+
"Service account email must follow the format: [email protected]"
265+
)
266+
|> unique_constraint(:email, name: :index_users_on_email)
267+
|> unique_constraint(:authentication_token, name: :index_users_on_authentication_token)
268+
|> unique_constraint(:idempotency_token, name: "users_idempotency_token_index")
269+
end
270+
271+
@doc """
272+
Generates a synthetic email for a service account.
273+
"""
274+
def generate_service_account_email(service_account_name, organization_name) do
275+
# Sanitize names to ensure valid email format
276+
sanitized_sa_name = sanitize_email_part(service_account_name)
277+
sanitized_org_name = sanitize_email_part(organization_name)
278+
279+
"#{sanitized_sa_name}@sa.#{sanitized_org_name}.semaphoreci.com"
280+
end
281+
282+
defp sanitize_email_part(name) do
283+
name
284+
|> String.downcase()
285+
|> String.replace(~r/[^a-z0-9\-]/, "-")
286+
|> String.replace(~r/-+/, "-")
287+
|> String.trim("-")
288+
end
232289
end

0 commit comments

Comments
 (0)