diff --git a/ee/rbac/assets/permissions.yaml b/ee/rbac/assets/permissions.yaml index 971a4119a..7f5c4e607 100644 --- a/ee/rbac/assets/permissions.yaml +++ b/ee/rbac/assets/permissions.yaml @@ -70,6 +70,10 @@ permissions: description: "View the existing dashboards within the organization." - name: "organization.dashboards.manage" description: "Create new dashboard views." + - name: "organization.service_accounts.view" + description: "View service accounts within the organization." + - name: "organization.service_accounts.manage" + description: "Manage service accounts within the organization." project: - name: "project.view" description: "Access the project. This permission is needed to see any page within the project." diff --git a/ee/rbac/assets/roles.yaml b/ee/rbac/assets/roles.yaml index a64e1ffcb..2a6c8c6ff 100644 --- a/ee/rbac/assets/roles.yaml +++ b/ee/rbac/assets/roles.yaml @@ -39,6 +39,8 @@ roles: - "organization.custom_roles.view" - "organization.dashboards.view" - "organization.dashboards.manage" + - "organization.service_accounts.view" + - "organization.service_accounts.manage" - name: "Admin" description: "Admins can modify settings within the organization or any of its projects. However, they do not have access to billing information, and they cannot change general organization details, such as the organization name and URL." maps_to: "Admin" @@ -77,6 +79,8 @@ roles: - "organization.dashboards.view" - "organization.dashboards.manage" - "project.delete" + - "organization.service_accounts.view" + - "organization.service_accounts.manage" - name: "Member" description: "Members can access the organization's homepage and the projects they are assigned to. However, they are not able to modify any settings." permissions: diff --git a/ee/rbac/lib/internal_api/audit.pb.ex b/ee/rbac/lib/internal_api/audit.pb.ex index 0dbf89fde..bda314bb1 100644 --- a/ee/rbac/lib/internal_api/audit.pb.ex +++ b/ee/rbac/lib/internal_api/audit.pb.ex @@ -67,6 +67,7 @@ defmodule InternalApi.Audit.Event.Resource do field(:Okta, 17) field(:FlakyTests, 18) field(:RBACRole, 19) + field(:ServiceAccount, 20) end defmodule InternalApi.Audit.Event.Operation do diff --git a/ee/rbac/lib/internal_api/rbac.pb.ex b/ee/rbac/lib/internal_api/rbac.pb.ex index 81e4eb9e0..5c7b4a4b4 100644 --- a/ee/rbac/lib/internal_api/rbac.pb.ex +++ b/ee/rbac/lib/internal_api/rbac.pb.ex @@ -5,6 +5,7 @@ defmodule InternalApi.RBAC.SubjectType do field(:USER, 0) field(:GROUP, 1) + field(:SERVICE_ACCOUNT, 2) end defmodule InternalApi.RBAC.Scope do diff --git a/ee/rbac/lib/internal_api/user.pb.ex b/ee/rbac/lib/internal_api/user.pb.ex index fe31c6d8c..7122a02c3 100644 --- a/ee/rbac/lib/internal_api/user.pb.ex +++ b/ee/rbac/lib/internal_api/user.pb.ex @@ -56,6 +56,7 @@ defmodule InternalApi.User.User.CreationSource do field(:NOT_SET, 0) field(:OKTA, 1) + field(:SERVICE_ACCOUNT, 2) end defmodule InternalApi.User.ListFavoritesRequest do diff --git a/ee/rbac/lib/rbac/grpc_servers/rbac_server.ex b/ee/rbac/lib/rbac/grpc_servers/rbac_server.ex index 3758f2ff9..ab8585b0f 100644 --- a/ee/rbac/lib/rbac/grpc_servers/rbac_server.ex +++ b/ee/rbac/lib/rbac/grpc_servers/rbac_server.ex @@ -371,7 +371,7 @@ defmodule Rbac.GrpcServers.RbacServer do total_pages: total_pages, members: Enum.map(subject_role_bindings, fn binding -> - subject_type = if binding.type == "user", do: :USER, else: :GROUP + subject_type = binding.type |> String.upcase() |> String.to_existing_atom() %RBAC.ListMembersResponse.Member{ subject: %RBAC.Subject{ diff --git a/front/Dockerfile b/front/Dockerfile index 0929df2a4..7765b402d 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -45,7 +45,7 @@ RUN mix sentry_recompile && mix compile --warnings-as-errors # -- elixir stage # -- node stage -FROM node:16-alpine as node +FROM node:16-alpine AS node WORKDIR /assets COPY front/assets/package.json front/assets/package-lock.json ./ RUN npm set progress=false && npm install diff --git a/front/assets/css/app-semaphore.css b/front/assets/css/app-semaphore.css index 3b61f3a2f..49988f443 100644 --- a/front/assets/css/app-semaphore.css +++ b/front/assets/css/app-semaphore.css @@ -2795,6 +2795,8 @@ img { max-width: 100%; } .b--indigo { border-color: #1570ff; } .b--dark-indigo { border-color: #00359f; } .b--orange { border-color: #fd7e14; } +.b--yellow { border-color: #FBC335; } +.b--blue { border-color: #2196F3; } .b--purple { border-color: #8658d6; } .b--dark-purple { border-color: #5122a5; } .b--dark-brown { border-color: #974510; } @@ -4585,6 +4587,7 @@ code, .code, pre { .bg-washed-purple { background-color: #f3ecff; } /* Yellows */ .yellow { color: #FBC335; } +.gold { color: #FBC335; } .lightest-yellow { color: #fff3bf; } .washed-yellow { color: #fffae4; } .bg-yellow { background-color: #FBC335; } diff --git a/front/assets/js/app.js b/front/assets/js/app.js index 7d8762e69..d2fd32f45 100644 --- a/front/assets/js/app.js +++ b/front/assets/js/app.js @@ -66,6 +66,7 @@ import { default as Agents} from "./agents"; import { default as AddPeople } from "./people/add_people"; import { default as EditPerson } from "./people/edit_person"; import { default as SyncPeople } from "./people/sync_people"; +import { default as ServiceAccounts } from "./service_accounts"; import { default as Report } from "./report"; import { InitializingScreen } from "./project_onboarding/initializing"; @@ -294,12 +295,23 @@ export var App = { GroupManagement.init(); new Star(); - const addPeopleAppRoot = document.getElementById("add-people"); - if (addPeopleAppRoot) { - AddPeople({ - dom: addPeopleAppRoot, - config: addPeopleAppRoot.dataset, - }); + + // Initialize Preact apps + const serviceAccountsEl = document.getElementById("service-accounts"); + if (serviceAccountsEl) { + const config = JSON.parse(serviceAccountsEl.dataset.config); + ServiceAccounts({ dom: serviceAccountsEl, config }); + } + + const addPeopleEl = document.getElementById("add-people"); + if (addPeopleEl) { + AddPeople({ dom: addPeopleEl, config: addPeopleEl.dataset }); + } + + const syncPeopleEl = document.querySelector(".app-sync-people"); + if (syncPeopleEl) { + const config = JSON.parse(syncPeopleEl.dataset.config); + SyncPeople({ dom: syncPeopleEl, config }); } document.querySelectorAll(".app-edit-person").forEach((editPersonAppRoot) => { @@ -516,6 +528,7 @@ export var App = { window.Notice.init(); + $(document).on("click", ".x-select-on-click", function (event) { event.currentTarget.setSelectionRange(0, event.currentTarget.value.length); }); diff --git a/front/assets/js/people/add_people/index.tsx b/front/assets/js/people/add_people/index.tsx index 7b28c5a1c..4ce823de7 100644 --- a/front/assets/js/people/add_people/index.tsx +++ b/front/assets/js/people/add_people/index.tsx @@ -44,7 +44,7 @@ export const App = () => { person_add {`Add people`} - close(false)} title="Add people"> + close(false)} title="Add new people" width="w-70-m"> @@ -85,7 +85,7 @@ const AddNewUsers = (props: { close: (reload: boolean) => void, }) => { const Link = (props: { icon: VNode, title: string, }) => { return ( void, }) => { }; return ( -
-
-

Add new people

-
- +
{userProviders.length > 1 && ( -
+
{userProviders.map(userProviderBox)}
)} @@ -158,7 +151,7 @@ const AddNewUsers = (props: { close: (reload: boolean) => void, }) => { return ( {loading && ( -
+
)} @@ -280,7 +273,7 @@ const ProvideVia = (props: ProvideViaProps) => { return (
{message}
-
+
)} {!arePeopleInvited && ( -
+
` + ${result.subject_type === "service_account" + ? `
smart_toy
` + : result.has_avatar + ? `` + : `
` } ${escapeHtml(result.name)} ` @@ -180,9 +190,11 @@ export var AddToProject = { `
- ${user.has_avatar - ? `` - : `` + ${user.subject_type === "service_account" + ? `
smart_toy
` + : user.has_avatar + ? `` + : `` }
${escapeHtml(user.name)}
diff --git a/front/assets/js/people/change_role_dropdown.js b/front/assets/js/people/change_role_dropdown.js index 16723274f..6c53fa9d7 100644 --- a/front/assets/js/people/change_role_dropdown.js +++ b/front/assets/js/people/change_role_dropdown.js @@ -67,7 +67,8 @@ export var ChangeRoleDropdown = { const body = { user_id: roleBtn.attributes.user_id.value, project_id: InjectedDataByBackend.ProjectId, - role_id: roleBtn.attributes.role_id.value + role_id: roleBtn.attributes.role_id.value, + member_type: roleBtn.attributes.member_type.value } toggleSpinner() diff --git a/front/assets/js/people/edit_person/index.tsx b/front/assets/js/people/edit_person/index.tsx index 0768868a6..6ebcf5d78 100644 --- a/front/assets/js/people/edit_person/index.tsx +++ b/front/assets/js/people/edit_person/index.tsx @@ -87,7 +87,7 @@ export const Button = () => { return user.assignRoleUrl .call({ - body: { user_id: user.id, role_id: selectedRole.id }, + body: { user_id: user.id, role_id: selectedRole.id, member_type: user.memberType }, }) .then((resp) => { if (resp.error) { @@ -163,53 +163,48 @@ export const Button = () => { manage_accounts Edit - -
-
-
-

Edit user

-
-
- -
- -
- + +
+
+ +
+
+ +
-
- - -
+
+ + +
-
- - - -
-
- + - -
+
@@ -418,6 +413,8 @@ class User { id: string; name: string; email: string; + memberType: string; + roles: UserRole[] = []; changeEmailUrl: toolbox.APIRequest.Url<{ email: string, message: string, }>; assignRoleUrl: toolbox.APIRequest.Url<{ password: string, message: string, }>; @@ -431,6 +428,7 @@ class User { user.id = json.id as string; user.name = json.name as string; user.email = json.email as string; + user.memberType = json.member_type as string; user.roles = json.roles.map((role: any) => { return new UserRole({ id: role.id, diff --git a/front/assets/js/service_accounts/components/CreateServiceAccount.tsx b/front/assets/js/service_accounts/components/CreateServiceAccount.tsx new file mode 100644 index 000000000..976ecd72d --- /dev/null +++ b/front/assets/js/service_accounts/components/CreateServiceAccount.tsx @@ -0,0 +1,139 @@ +import { useState, useContext } from "preact/hooks"; +import { Modal } from "js/toolbox"; +import { ConfigContext } from "../config"; +import { ServiceAccountsAPI } from "../utils/api"; +import { TokenDisplay } from "./TokenDisplay"; +import * as toolbox from "js/toolbox"; + +interface CreateServiceAccountProps { + isOpen: boolean; + onClose: () => void; + onCreated: () => void; +} + +export const CreateServiceAccount = ({ isOpen, onClose, onCreated }: CreateServiceAccountProps) => { + const config = useContext(ConfigContext); + const api = new ServiceAccountsAPI(config); + + const [name, setName] = useState(``); + const [description, setDescription] = useState(``); + const [selectedRoleId, setSelectedRoleId] = useState(``); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(``); + const [token, setToken] = useState(``); + + const handleSubmit = async (e: Event) => { + e.preventDefault(); + setError(null); + setLoading(true); + + const response = await api.create(name, description, selectedRoleId); + + if (response.error) { + setError(response.error); + setLoading(false); + } else if (response.data) { + setToken(response.data.api_token); + setLoading(false); + } + }; + + const handleClose = () => { + if (token) { + onCreated(); + } + setName(``); + setDescription(``); + setSelectedRoleId(``); + setError(``); + setToken(``); + onClose(); + }; + + const canSubmit = name.trim().length > 0 && selectedRoleId.length > 0 && !loading; + + return ( + + {!token ? ( +
void handleSubmit(e)}> +
+
+ + setName(e.currentTarget.value)} + placeholder="e.g., CI/CD Pipeline" + disabled={loading} + autoFocus + /> +
+ +
+ +