-
Notifications
You must be signed in to change notification settings - Fork 83
Expand file tree
/
Copy pathcredential_controller.ex
More file actions
250 lines (198 loc) · 7.09 KB
/
Copy pathcredential_controller.ex
File metadata and controls
250 lines (198 loc) · 7.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
defmodule LightningWeb.API.CredentialController do
@moduledoc """
API controller for credential management.
Handles creation, retrieval, and deletion of credentials. Credentials are
used to authenticate with external services and can be associated with
multiple projects.
## Security
- Credential bodies are excluded from responses for security
- Users can only delete credentials they own
- Project access is required to view project credentials
## Examples
GET /api/credentials
GET /api/credentials?project_id=a1b2c3d4-...
POST /api/credentials
DELETE /api/credentials/a1b2c3d4-...
"""
use LightningWeb, :controller
alias Lightning.Credentials
alias Lightning.Policies.Permissions
alias Lightning.Policies.ProjectUsers
alias Lightning.Projects
alias LightningWeb.API.Helpers
action_fallback LightningWeb.FallbackController
@doc """
Lists credentials with optional project filtering.
This function has two variants:
- With `project_id`: Returns all credentials for a specific project (regardless of owner)
- Without `project_id`: Returns only credentials owned by the authenticated user
Credential bodies are excluded from responses for security.
## Parameters
- `conn` - The Plug connection struct with the current resource assigned
- `params` - Map containing:
- `project_id` - Project UUID (optional, filters to specific project)
## Returns
- `200 OK` with list of credentials (bodies excluded)
- `404 Not Found` if project doesn't exist (when project_id provided)
- `403 Forbidden` if user lacks project access (when project_id provided)
## Examples
# User's own credentials
GET /api/credentials
# All credentials for a project
GET /api/credentials?project_id=a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
"""
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
def index(conn, %{"project_id" => project_id}) do
current_user = conn.assigns.current_resource
with :ok <- Helpers.validate_uuid(project_id),
project when not is_nil(project) <- Projects.get_project(project_id),
:ok <-
ProjectUsers
|> Permissions.can(
:access_project,
current_user,
project
) do
credentials = Credentials.list_credentials(project)
render(conn, "index.json", credentials: credentials)
else
{:error, :bad_request} ->
{:error, :bad_request}
nil ->
{:error, :not_found}
{:error, :unauthorized} ->
{:error, :forbidden}
end
end
def index(conn, _params) do
current_user = conn.assigns.current_resource
credentials = Credentials.list_credentials(current_user)
render(conn, "index.json", credentials: credentials)
end
@doc """
Creates a new credential and optionally grants it access to projects.
Creates a credential owned by the authenticated user. If project_credentials
are specified, the user must have access to all listed projects. The credential
body is included in the response only upon creation.
## Parameters
- `conn` - The Plug connection struct with the current resource assigned
- `params` - Map containing:
- `name` - Credential name (required)
- `body` - Credential JSON body with authentication details (required)
- `project_credentials` - List of project associations (optional)
## Returns
- `201 Created` with credential JSON including body
- `422 Unprocessable Entity` on validation errors
- `403 Forbidden` if user lacks access to specified projects
## Examples
# Create credential without project association
POST /api/credentials
{
"name": "My API Key",
"body": {"apiKey": "secret123"}
}
# Create credential with project associations
POST /api/credentials
{
"name": "Shared Credential",
"body": {"token": "abc123"},
"project_credentials": [
{"project_id": "a1b2c3d4-..."}
]
}
"""
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
def create(conn, params) do
current_user = conn.assigns.current_resource
with {:ok, validated_params} <-
validate_and_authorize_projects(params, current_user),
{:ok, credential} <- Credentials.create_credential(validated_params) do
conn
|> put_status(:created)
|> render("create.json", credential: credential)
end
end
@doc """
Deletes a credential owned by the authenticated user.
Permanently removes a credential. Only the credential owner can delete it.
Credentials in use by workflows cannot be deleted and will return an error.
## Parameters
- `conn` - The Plug connection struct with the current resource assigned
- `params` - Map containing:
- `id` - Credential UUID (required)
## Returns
- `204 No Content` on successful deletion
- `404 Not Found` if credential doesn't exist or invalid UUID
- `403 Forbidden` if user is not the credential owner
## Examples
DELETE /api/credentials/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
"""
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
def delete(conn, %{"id" => id}) do
current_user = conn.assigns.current_resource
with :ok <- Helpers.validate_uuid(id),
credential when not is_nil(credential) <-
Credentials.get_credential(id),
:ok <- validate_credential_ownership(credential, current_user),
{:ok, _} <- Credentials.delete_credential(credential) do
send_resp(conn, :no_content, "")
else
{:error, :bad_request} ->
{:error, :bad_request}
nil ->
{:error, :not_found}
{:error, :forbidden} ->
{:error, :forbidden}
error ->
error
end
end
defp validate_credential_ownership(credential, current_user) do
if credential.user_id == current_user.id do
:ok
else
{:error, :forbidden}
end
end
defp validate_and_authorize_projects(params, current_user) do
# Ensure user_id is set to the current authenticated user
params_with_user = Map.put(params, "user_id", current_user.id)
project_credentials = Map.get(params, "project_credentials", [])
if Enum.empty?(project_credentials) do
{:ok, params_with_user}
else
case validate_project_access(project_credentials, current_user) do
:ok -> {:ok, params_with_user}
{:error, _} = error -> error
end
end
end
defp validate_project_access(project_credentials, current_user) do
project_ids =
project_credentials
|> Enum.map(&Map.get(&1, "project_id"))
|> Enum.filter(& &1)
unauthorized_projects =
Enum.filter(project_ids, fn project_id ->
case Projects.get_project(project_id) do
nil ->
true
project ->
case ProjectUsers
|> Permissions.can(
:create_project_credential,
current_user,
project
) do
:ok -> false
_ -> true
end
end
end)
if Enum.empty?(unauthorized_projects) do
:ok
else
{:error, :forbidden}
end
end
end