diff --git a/assets/package.json b/assets/package.json index 93a871eb27..c62c98fd40 100644 --- a/assets/package.json +++ b/assets/package.json @@ -32,6 +32,7 @@ "@apollo/client": "3.7.15", "@emotion/react": "11.11.1", "@emotion/styled": "11.11.0", + "@graphql-codegen/named-operations-object": "^2.3.1", "@jumpn/utils-graphql": "0.6.0", "@loomhq/loom-embed": "1.5.0", "@markdoc/markdoc": "0.3.0", diff --git a/assets/src/components/apps/app/App.tsx b/assets/src/components/apps/app/App.tsx index 3b8de5c53f..c6ab825f74 100644 --- a/assets/src/components/apps/app/App.tsx +++ b/assets/src/components/apps/app/App.tsx @@ -112,7 +112,7 @@ export const getDirectory = ({ label: 'Cost analysis', enabled: app?.cost || app?.license, }, - { path: 'oidc', label: 'User management', enabled: true }, + { path: 'oidc', label: 'OpenID user management', enabled: true }, { path: 'credentials', label: 'Credentials', diff --git a/assets/src/components/apps/app/oidc/UserManagement.tsx b/assets/src/components/apps/app/oidc/UserManagement.tsx index df9d7e010c..cacc20f0c9 100644 --- a/assets/src/components/apps/app/oidc/UserManagement.tsx +++ b/assets/src/components/apps/app/oidc/UserManagement.tsx @@ -278,7 +278,7 @@ export default function UserManagement() { return ( diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index 3cb0e1b87f..14a6515db4 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -1,4 +1,5 @@ /* eslint-disable */ +/* prettier-ignore */ import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; export type Maybe = T | null; @@ -611,6 +612,7 @@ export type HttpIngressRule = { export type Ingress = { __typename?: 'Ingress'; + certificates?: Maybe>>; events?: Maybe>>; metadata: Metadata; raw: Scalars['String']['output']; @@ -1209,6 +1211,7 @@ export type RootMutationType = { createRole?: Maybe; createUpgradePolicy?: Maybe; createWebhook?: Maybe; + deleteCertificate?: Maybe; deleteGroup?: Maybe; deleteGroupMember?: Maybe; deleteJob?: Maybe; @@ -1217,6 +1220,7 @@ export type RootMutationType = { deletePod?: Maybe; deleteRole?: Maybe; deleteUpgradePolicy?: Maybe; + deleteUser?: Maybe; deleteWebhook?: Maybe; executeRunbook?: Maybe; installRecipe?: Maybe; @@ -1290,6 +1294,12 @@ export type RootMutationTypeCreateWebhookArgs = { }; +export type RootMutationTypeDeleteCertificateArgs = { + name: Scalars['String']['input']; + namespace: Scalars['String']['input']; +}; + + export type RootMutationTypeDeleteGroupArgs = { groupId: Scalars['ID']['input']; }; @@ -1333,6 +1343,11 @@ export type RootMutationTypeDeleteUpgradePolicyArgs = { }; +export type RootMutationTypeDeleteUserArgs = { + id: Scalars['ID']['input']; +}; + + export type RootMutationTypeDeleteWebhookArgs = { id: Scalars['ID']['input']; }; @@ -3067,4 +3082,43 @@ export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions; export type MeLazyQueryHookResult = ReturnType; -export type MeQueryResult = Apollo.QueryResult; \ No newline at end of file +export type MeQueryResult = Apollo.QueryResult; +export const namedOperations = { + Query: { + App: 'App', + AppInfo: 'AppInfo', + Repository: 'Repository', + PluralContext: 'PluralContext', + Groups: 'Groups', + SearchGroups: 'SearchGroups', + GroupMembers: 'GroupMembers', + Me: 'Me' + }, + Mutation: { + CreateBuild: 'CreateBuild', + CreateGroupMember: 'CreateGroupMember', + DeleteGroupMember: 'DeleteGroupMember', + CreateGroup: 'CreateGroup', + UpdateGroup: 'UpdateGroup', + DeleteGroup: 'DeleteGroup' + }, + Fragment: { + CostAnalysisFragment: 'CostAnalysisFragment', + FileContentFragment: 'FileContentFragment', + ConfigurationFragment: 'ConfigurationFragment', + ApplicationSpecFragment: 'ApplicationSpecFragment', + ApplicationStatusFragment: 'ApplicationStatusFragment', + ApplicationFragment: 'ApplicationFragment', + MetadataFragment: 'MetadataFragment', + ConfigurationOverlayFragment: 'ConfigurationOverlayFragment', + RepositoryFragment: 'RepositoryFragment', + PageInfo: 'PageInfo', + GroupMember: 'GroupMember', + Group: 'Group', + User: 'User', + Invite: 'Invite', + RoleBinding: 'RoleBinding', + Role: 'Role', + Manifest: 'Manifest' + } +} \ No newline at end of file diff --git a/assets/yarn.lock b/assets/yarn.lock index d3454a9819..90685d007e 100644 --- a/assets/yarn.lock +++ b/assets/yarn.lock @@ -2717,7 +2717,21 @@ __metadata: languageName: node linkType: hard -"@graphql-codegen/plugin-helpers@npm:^2.7.2": +"@graphql-codegen/named-operations-object@npm:^2.3.1": + version: 2.3.1 + resolution: "@graphql-codegen/named-operations-object@npm:2.3.1" + dependencies: + "@graphql-codegen/plugin-helpers": ^2.6.2 + change-case-all: 1.0.14 + tslib: ~2.4.0 + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-tag: ^2.0.0 + checksum: 927240fc3d06b87de6ea61d299c89a7c49b166822814d32f0a7dd552c963faa31a57a3dbe56ca091bfc70275e4a13318854168cb1d8107bbbbc9abd7119ae593 + languageName: node + linkType: hard + +"@graphql-codegen/plugin-helpers@npm:^2.6.2, @graphql-codegen/plugin-helpers@npm:^2.7.2": version: 2.7.2 resolution: "@graphql-codegen/plugin-helpers@npm:2.7.2" dependencies: @@ -8433,6 +8447,7 @@ __metadata: "@graphql-codegen/add": 5.0.0 "@graphql-codegen/cli": 4.0.1 "@graphql-codegen/introspection": 4.0.0 + "@graphql-codegen/named-operations-object": ^2.3.1 "@graphql-codegen/typescript": 4.0.0 "@graphql-codegen/typescript-operations": 4.0.0 "@graphql-codegen/typescript-react-apollo": 3.3.7 diff --git a/lib/console/graphql/audit.ex b/lib/console/graphql/audit.ex index 4592378182..c6dc6ae234 100644 --- a/lib/console/graphql/audit.ex +++ b/lib/console/graphql/audit.ex @@ -1,7 +1,6 @@ defmodule Console.GraphQl.Audit do use Console.GraphQl.Schema.Base alias Console.GraphQl.Resolvers.{User, Audit} - alias Console.Middleware.{Authenticated} alias Console.Schema ecto_enum :audit_type, Schema.Audit.Type diff --git a/lib/console/graphql/build.ex b/lib/console/graphql/build.ex index bdc27a30ba..0274ea87d4 100644 --- a/lib/console/graphql/build.ex +++ b/lib/console/graphql/build.ex @@ -1,7 +1,7 @@ defmodule Console.GraphQl.Build do use Console.GraphQl.Schema.Base alias Console.Schema - alias Console.Middleware.{Authenticated, RequiresGit, Rbac} + alias Console.Middleware.{RequiresGit} alias Console.GraphQl.Resolvers.{Build, User} ecto_enum :status, Schema.Build.Status diff --git a/lib/console/graphql/configuration.ex b/lib/console/graphql/configuration.ex index 1045ab488d..05f56ae8f0 100644 --- a/lib/console/graphql/configuration.ex +++ b/lib/console/graphql/configuration.ex @@ -1,7 +1,7 @@ defmodule Console.GraphQl.Configuration do use Console.GraphQl.Schema.Base require Logger - alias Console.Middleware.{Authenticated, Sandboxed} + alias Console.Middleware.{Sandboxed} alias Console.GraphQl.Resolvers.Plural object :configuration do diff --git a/lib/console/graphql/kubernetes.ex b/lib/console/graphql/kubernetes.ex index df5e8e2b0c..f8b05c3a8b 100644 --- a/lib/console/graphql/kubernetes.ex +++ b/lib/console/graphql/kubernetes.ex @@ -283,6 +283,8 @@ defmodule Console.GraphQl.Kubernetes do safe_resolve &VPN.delete_peer/2 end + + import_fields :certificate_mutations end object :kubernetes_subscriptions do diff --git a/lib/console/graphql/kubernetes/application.ex b/lib/console/graphql/kubernetes/application.ex index a7d241491d..45f9766fb9 100644 --- a/lib/console/graphql/kubernetes/application.ex +++ b/lib/console/graphql/kubernetes/application.ex @@ -1,6 +1,5 @@ defmodule Console.GraphQl.Kubernetes.Application do use Console.GraphQl.Schema.Base - alias Console.Middleware.{Rbac} alias Console.GraphQl.Resolvers.{Plural, Kubecost, License} object :application do diff --git a/lib/console/graphql/kubernetes/certificate.ex b/lib/console/graphql/kubernetes/certificate.ex index 6a68d133f4..957621c673 100644 --- a/lib/console/graphql/kubernetes/certificate.ex +++ b/lib/console/graphql/kubernetes/certificate.ex @@ -30,4 +30,16 @@ defmodule Console.GraphQl.Kubernetes.Certificate do field :kind, :string field :name, :string end + + object :certificate_mutations do + field :delete_certificate, :boolean do + middleware Authenticated + middleware AdminRequired + + arg :name, non_null(:string) + arg :namespace, non_null(:string) + + safe_resolve &Kubernetes.delete_certificate/2 + end + end end diff --git a/lib/console/graphql/kubernetes/ingress.ex b/lib/console/graphql/kubernetes/ingress.ex index 02cd3dbe24..eda937cd46 100644 --- a/lib/console/graphql/kubernetes/ingress.ex +++ b/lib/console/graphql/kubernetes/ingress.ex @@ -8,6 +8,8 @@ defmodule Console.GraphQl.Kubernetes.Ingress do field :status, non_null(:service_status) field :spec, non_null(:ingress_spec) + field :certificates, list_of(:certificate), resolve: fn model, _, _ -> Kubernetes.ingress_certificates(model) end + field :raw, non_null(:string), resolve: fn model, _, _ -> encode(model) end field :events, list_of(:event), resolve: fn model, _, _ -> Kubernetes.list_events(model) end end diff --git a/lib/console/graphql/observability.ex b/lib/console/graphql/observability.ex index 2a176c9e9d..122ece2d48 100644 --- a/lib/console/graphql/observability.ex +++ b/lib/console/graphql/observability.ex @@ -1,7 +1,6 @@ defmodule Console.GraphQl.Observability do use Console.GraphQl.Schema.Base alias Console.GraphQl.Resolvers.Observability - alias Console.Middleware.{Authenticated, Rbac} enum :autoscaling_target do value :statefulset diff --git a/lib/console/graphql/plural.ex b/lib/console/graphql/plural.ex index 561c72cec9..11fc66393c 100644 --- a/lib/console/graphql/plural.ex +++ b/lib/console/graphql/plural.ex @@ -1,7 +1,7 @@ defmodule Console.GraphQl.Plural do use Console.GraphQl.Schema.Base alias Console.GraphQl.Resolvers.Plural - alias Console.Middleware.{Authenticated, AdminRequired, RequiresGit, Rbac} + alias Console.Middleware.{RequiresGit} input_object :smtp_input do field :server, :string diff --git a/lib/console/graphql/policies.ex b/lib/console/graphql/policies.ex index ffa306b528..fa6b2ccfac 100644 --- a/lib/console/graphql/policies.ex +++ b/lib/console/graphql/policies.ex @@ -1,6 +1,5 @@ defmodule Console.GraphQl.Policies do use Console.GraphQl.Schema.Base - alias Console.Middleware.{Authenticated, AdminRequired} alias Console.GraphQl.Resolvers.Policy ecto_enum :upgrade_policy_type, Console.Schema.UpgradePolicy.Type diff --git a/lib/console/graphql/resolvers/kubernetes.ex b/lib/console/graphql/resolvers/kubernetes.ex index cb440897e4..0d23ac3302 100644 --- a/lib/console/graphql/resolvers/kubernetes.ex +++ b/lib/console/graphql/resolvers/kubernetes.ex @@ -80,6 +80,14 @@ defmodule Console.GraphQl.Resolvers.Kubernetes do Client.get_certificate(ns, name) end + def ingress_certificates(%{metadata: %{namespace: ns}, spec: %{tls: [_ | _] = tls}}) do + names = MapSet.new(tls, & &1.secret_name) + with {:ok, %{items: certs}} <- Client.list_certificate(ns) do + {:ok, %{items: Enum.filter(certs, &MapSet.member?(names, &1.metadata.name))}} + end + end + def ingress_certificates(_), do: {:ok, []} + def list_nodes(_, _) do Core.list_node!() |> Kazan.run() @@ -123,6 +131,11 @@ defmodule Console.GraphQl.Resolvers.Kubernetes do |> Kazan.run() end + def delete_certificate(%{namespace: ns, name: name}, _) do + with {:ok, _} <- Client.delete_certificate(ns, name), + do: {:ok, true} + end + def list_events(%{metadata: %{uid: uid, namespace: ns}}) do Console.namespace(ns) |> Core.list_namespaced_event!(field_selector: "involvedObject.uid=#{uid}") diff --git a/lib/console/graphql/resolvers/user.ex b/lib/console/graphql/resolvers/user.ex index c8f02454fa..ff2369d054 100644 --- a/lib/console/graphql/resolvers/user.ex +++ b/lib/console/graphql/resolvers/user.ex @@ -121,6 +121,9 @@ defmodule Console.GraphQl.Resolvers.User do def update_user(%{attributes: attrs}, %{context: %{current_user: user}}), do: Users.update_user(attrs, user) + def delete_user(%{id: id}, %{context: %{current_user: user}}), + do: Users.delete_user(id, user) + def create_invite(%{attributes: attrs}, _), do: Users.create_invite(attrs) diff --git a/lib/console/graphql/runbooks.ex b/lib/console/graphql/runbooks.ex index 6cdd3eb994..058000c803 100644 --- a/lib/console/graphql/runbooks.ex +++ b/lib/console/graphql/runbooks.ex @@ -1,6 +1,6 @@ defmodule Console.GraphQl.Runbooks do use Console.GraphQl.Schema.Base - alias Console.Middleware.{Authenticated, RequiresGit, Rbac} + alias Console.Middleware.{RequiresGit} alias Console.GraphQl.Resolvers.{Runbooks, User} alias Kazan.Apis.Apps.V1, as: AppsV1 diff --git a/lib/console/graphql/schema/base.ex b/lib/console/graphql/schema/base.ex index 0c45062c28..ef767a2031 100644 --- a/lib/console/graphql/schema/base.ex +++ b/lib/console/graphql/schema/base.ex @@ -19,6 +19,7 @@ defmodule Console.GraphQl.Schema.Base do use Absinthe.Relay.Schema.Notation, :modern import Absinthe.Resolution.Helpers import Console.GraphQl.Schema.Base + alias Console.Middleware.{Authenticated, AdminRequired, Rbac, Feature} end end diff --git a/lib/console/graphql/users.ex b/lib/console/graphql/users.ex index fc15bfd620..5f33e23906 100644 --- a/lib/console/graphql/users.ex +++ b/lib/console/graphql/users.ex @@ -1,7 +1,7 @@ defmodule Console.GraphQl.Users do use Console.GraphQl.Schema.Base alias Console.GraphQl.Resolvers.User - alias Console.Middleware.{Authenticated, AdminRequired, AllowJwt, Sandboxed} + alias Console.Middleware.{AllowJwt, Sandboxed} alias Console.Schema.Notification.{Severity, Status} enum_from_list :permission, Console.Schema.Role, :permissions, [] @@ -274,6 +274,14 @@ defmodule Console.GraphQl.Users do safe_resolve &User.update_user/2 end + field :delete_user, :user do + middleware Authenticated + middleware AdminRequired + arg :id, non_null(:id) + + safe_resolve &User.delete_user/2 + end + field :mark_read, :user do middleware Authenticated arg :type, :read_type diff --git a/lib/console/graphql/webhooks.ex b/lib/console/graphql/webhooks.ex index e34b409466..4da756aba4 100644 --- a/lib/console/graphql/webhooks.ex +++ b/lib/console/graphql/webhooks.ex @@ -1,7 +1,7 @@ defmodule Console.GraphQl.Webhooks do use Console.GraphQl.Schema.Base alias Console.GraphQl.Resolvers.Webhook - alias Console.Middleware.{Authenticated, Sandboxed} + alias Console.Middleware.{Sandboxed} alias Console.Schema ecto_enum :webhook_type, Schema.Webhook.Type diff --git a/lib/console/services/users.ex b/lib/console/services/users.ex index 696eaa5f34..20539f2386 100644 --- a/lib/console/services/users.ex +++ b/lib/console/services/users.ex @@ -77,6 +77,13 @@ defmodule Console.Services.Users do |> notify(:create) end + @spec delete_user(binary, User.t) :: user_resp + def delete_user(id, %User{id: id}), do: {:error, "you cannot delete yourself"} + def delete_user(id, %User{}) do + get_user!(id) + |> Repo.delete() + end + @spec bootstrap_user(map) :: user_resp def bootstrap_user(%{"email" => email} = attrs) do attrs = token_attrs(attrs) diff --git a/lib/kube/client.ex b/lib/kube/client.ex index 3e0b366113..8456ed0a63 100644 --- a/lib/kube/client.ex +++ b/lib/kube/client.ex @@ -9,6 +9,7 @@ defmodule Kube.Client do list_request :list_runbooks, Kube.RunbookList, "platform.plural.sh", "v1alpha1", "runbooks" list_request :list_vertical_pod_autoscalers, Kube.VerticalPodAutoscalerList, "autoscaling.k8s.io", "v1", "verticalpodautoscalers" list_request :list_wireguard_peers, Kube.WireguardPeerList, "vpn.plural.sh", "v1alpha1", "wireguardpeers" + list_request :list_certificate, Kube.Certificate, "cert-manager.io", "v1", "certificates" get_request :get_dashboard, Kube.Dashboard, "platform.plural.sh", "v1alpha1", "dashboards" get_request :get_slashcommand, Kube.SlashCommand, "platform.plural.sh", "v1alpha1", "slashcommands" @@ -21,6 +22,7 @@ defmodule Kube.Client do get_request :get_wireguard_server, Kube.WireguardServer, "vpn.plural.sh", "v1alpha1", "wireguardservers" delete_request :delete_wireguard_peer, "vpn.plural.sh", "v1alpha1", "wireguardpeers" + delete_request :delete_certificate, "cert-manager.io", "v1", "certificates" def get_application(name), do: get_application(name, name) diff --git a/schema/schema.graphql b/schema/schema.graphql index 21d47cbda9..ff04283163 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -777,6 +777,7 @@ type RootMutationType { oauthCallback(code: String!, redirect: String): User createInvite(attributes: InviteAttributes!): Invite updateUser(id: ID, attributes: UserAttributes!): User + deleteUser(id: ID!): User markRead(type: ReadType): User createGroup(attributes: GroupAttributes!): Group deleteGroup(groupId: ID!): Group @@ -786,6 +787,7 @@ type RootMutationType { createRole(attributes: RoleAttributes!): Role updateRole(id: ID!, attributes: RoleAttributes!): Role deleteRole(id: ID!): Role + deleteCertificate(name: String!, namespace: String!): Boolean deletePod(namespace: String!, name: String!): Pod deleteJob(namespace: String!, name: String!): Job deleteNode(name: String!): Node @@ -1057,6 +1059,7 @@ type Ingress { metadata: Metadata! status: ServiceStatus! spec: IngressSpec! + certificates: [Certificate] raw: String! events: [Event] } @@ -1284,11 +1287,6 @@ type ContainerState { waiting: WaitingState } -input RunbookActionInput { - action: String! - context: Map! -} - type RepositoryContext { repository: String! context: Map @@ -1314,6 +1312,11 @@ input LabelInput { value: String } +input RunbookActionInput { + action: String! + context: Map! +} + type Dashboard { id: String! spec: DashboardSpec! diff --git a/test/console/graphql/mutations/kubernetes_mutations_test.exs b/test/console/graphql/mutations/kubernetes_mutations_test.exs index 0a6da4891d..30e6eb70af 100644 --- a/test/console/graphql/mutations/kubernetes_mutations_test.exs +++ b/test/console/graphql/mutations/kubernetes_mutations_test.exs @@ -113,6 +113,29 @@ defmodule Console.GraphQl.KubernetesMutationsTest do end end + describe "deleteCertificate" do + test "admins can delete certificates" do + admin = admin_user() + expect(Kazan, :run, fn _ -> {:ok, certificate("test")} end) + + {:ok, %{data: %{"deleteCertificate" => true}}} = run_query(""" + mutation Delete($name: String!, $namespace: String!) { + deleteCertificate(name: $name, namespace: $namespace) + } + """, %{"name" => "test", "namespace" => "ns"}, %{current_user: admin}) + end + + test "nonadmins cannot delete certificates" do + user = insert(:user) + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation Delete($name: String!, $namespace: String!) { + deleteCertificate(name: $name, namespace: $namespace) + } + """, %{"name" => "test", "namespace" => "ns"}, %{current_user: user}) + end + end + describe "deletePeer" do test "admins can delete wireguard peers" do admin = insert(:user, roles: %{admin: true}) diff --git a/test/console/graphql/mutations/user_mutations_test.exs b/test/console/graphql/mutations/user_mutations_test.exs index 02c3259135..3cb40f0f75 100644 --- a/test/console/graphql/mutations/user_mutations_test.exs +++ b/test/console/graphql/mutations/user_mutations_test.exs @@ -118,6 +118,46 @@ defmodule Console.GraphQl.UserMutationsTest do end end + describe "deleteUser" do + test "admins can delete other users" do + admin = admin_user() + user = insert(:user) + + {:ok, %{data: %{"deleteUser" => del}}} = run_query(""" + mutation Delete($id: ID!) { + deleteUser(id: $id) { id } + } + """, %{"id" => user.id}, %{current_user: admin}) + + assert del["id"] == user.id + refute refetch(user) + end + + test "admins cannot delete themselves" do + admin = admin_user() + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation Delete($id: ID!) { + deleteUser(id: $id) { id } + } + """, %{"id" => admin.id}, %{current_user: admin}) + + assert refetch(admin) + end + + test "nonadmins cannot delete users" do + [user, other] = insert_list(2, :user) + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation Delete($id: ID!) { + deleteUser(id: $id) { id } + } + """, %{"id" => other.id}, %{current_user: user}) + + assert refetch(user) + end + end + describe "createGroup" do test "it can create a group" do admin = insert(:user, roles: %{admin: true}) diff --git a/test/support/factory.ex b/test/support/factory.ex index 289d404c51..2390aa4382 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -138,4 +138,8 @@ defmodule Console.Factory do role = insert(:role, repositories: repos, permissions: Map.new(perms)) insert(:role_binding, role: role, user: user) end + + def admin_user() do + insert(:user, roles: %{admin: true}) + end end