Skip to content

Commit 270d1b3

Browse files
committed
Migrate 'sandboxing user namespace using app projects' doc
1 parent 9d18356 commit 270d1b3

File tree

1 file changed

+208
-0
lines changed

1 file changed

+208
-0
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# Sandboxing RHTAP user namespaces with AppProject
2+
3+
### Written by
4+
- Jonathan West (@jgwest)
5+
- Originally written in April 14, 2023
6+
7+
8+
This document examines the implementation details of how to add support for constraining RHTAP/Core GitOps Service users within an AppProject.
9+
10+
Feature: [GITOPSRVCE-428](https://issues.redhat.com/browse/GITOPSRVCE-428)
11+
Epic: [GITOPSRVCE-56](https://issues.redhat.com/browse/GITOPSRVCE-56)
12+
13+
# Introduction
14+
15+
An **API namespace** is a Kubernetes Namespace where users of RHTAP can create RHTAP API CRs, and deploy their Applications.
16+
17+
* These are the '\*-tenant' Namespaces on the cluster.
18+
* For example, ‘jgwest-tenant' is mine. (Based on your Red Hat ID)
19+
20+
Each API namespace has a corresponding **ClusterUser** row in the database. ClusterUser is a simple mechanism for tracking individual users within the GitOps Service (this mechanism may evolve over time, for example, to support other ownership concepts, but currently fits our needs in RHTAP).
21+
22+
* There is a 1-1 relationship between a user’s API Namespace and ClusterUser row
23+
* For each user’s API namespace, there is a corresponding ClusterUser row that corresponds to that namespace, and vice versa.
24+
25+
Thus, when thinking about how to constrain users, we can use the ClusterUser row (and relationships to that row) to represent all of the permissions that a user should have.
26+
27+
The ClusterUser table is very simple:
28+
29+
* User\_name string (reference to the user’s API namespace)
30+
* Created\_on timestamp (when the ClusterUser row was added added to the DB)
31+
32+
# Our Goal
33+
34+
Since what we’re discussing here is how to constrain users with an AppProject, it makes sense that there should exist one Argo CD AppProject per user.
35+
36+
An AppProject for a user would look something like this:
37+
38+
```yaml
39+
apiVersion: argoproj.io/v1alpha1
40+
kind: AppProject
41+
metadata:
42+
name: app-project-(cluster user uuid)
43+
annotation:
44+
username: (name of namespace of the user)
45+
namespace: gitops-service-argocd
46+
47+
spec:
48+
49+
sourceRepos:
50+
# each entry in this list would correspond to a AppProjectRepository row
51+
- all of the repo URLs that a user has access to
52+
53+
destinations:
54+
# each entry in this list would correspond to a AppProjectManagedEnvironment row
55+
- namespace: '*'
56+
server: (references to Argo CD cluster secret, which itself is related to a single ManagedEnv)
57+
58+
# (there are other AppProject restrictions we can add, but I’m focused on the above 2 for now)
59+
```
60+
61+
The GitOps Service (specifically the *cluster-agent* component, which is the component which interacts with Argo CD) needs to ensure that such an AppProject is created for each user, and that the **sourceRepos** and **destinations** fields are consistent with that user’s current access.
62+
63+
* As above, there is a 1-1 relationship between a user, and an AppProject.
64+
* Each RHTAP user should have a single corresponding AppProject.
65+
66+
Next, once each user has a corresponding AppProject within the Argo CD namespace that restricts their access, we should ensure that all Argo CD Applications that are generated for that user have a ‘project:’ field that references that AppProject.
67+
68+
```yaml
69+
apiVersion: argoproj.io/v1alpha1
70+
kind: Application
71+
metadata:
72+
name: guestbook
73+
namespace: argocd
74+
spec:
75+
project: (name of app project resource, above) # <==
76+
77+
source:
78+
repoURL: (...)
79+
80+
destination:
81+
server: (...)
82+
namespace: (...)
83+
```
84+
85+
# Argo CD’s default Repository Credential behaviour is not a good fit for multitenancy, and AppProject doesn’t fix that
86+
87+
The way that Argo CD handles repository credentials does not fit well with multitenancy. This is covered [in this document](argo-cd-multitenancy-for-fully-untrusted-users.md).
88+
89+
In short, if one Argo CD Application has the credentials to read from a private Git repository, then ANY Argo CD Application can read from that Git repository (and will use those credentials).
90+
91+
* Ostensibly this is because Argo CD was designed with the idea that Argo CD administrators would determine which users can/should access which repositories, which in a private organization (a large company) would be typical.
92+
93+
As of this writing, the current short-term solution to this (and which is unrelated to this feature) is to ensure that only a single user can ever reference a single repository URL. For example, this would be invalid:
94+
95+
* User A creates a GitOpsDeployment targeting private repo [https://github.com/user-a/private-repo](https://github.com/user-a/private-repo)
96+
* GitOps Service connects to the repo using the GitOpsDeploymentRepositoryCredentials, and configures Argo CD to use it.
97+
* User B creates a GitOpsDeployment targeting the exact same URL, [https://github.com/user-a/private-repo](https://github.com/user-a/private-repo) (which they may or may not actually have valid credentials for)
98+
* GitOps Service rejects this repository, and does not configure Argo CD to use it, because another user is already targeting it
99+
* (But note there are race condition/ordering issues that need to be solved if we want to reject only the first instance of a repository)
100+
101+
# Implementation Details
102+
103+
## Changes to backend/backend-shared components
104+
105+
**New database table \- AppProjectRepository:**
106+
107+
* ClusterUser (foreign key)
108+
* DB index on this field, to allow us to quickly retrieve all items for a particular user.
109+
* RepositoryCredential (foreign key, nullable)
110+
* If there exist private credentials for this repository URL, they will be referenced by this field.
111+
* Note: there may not exist private credentials for a repository URL, because, for example, the Git repository is public (and therefore a GitOpsDeploymentRepositoryCredential is not needed).
112+
* Normalized repository URL (string, non-nullable)
113+
* (Argo CD has a function we can use for normalization of the repo URL)
114+
115+
This row tracks which Git repository URLs a particular user can access.
116+
117+
There should also exist a uniqueness constraint on the *(cluster user, normalized repository url) tuple*, to eliminate the case where multiple AppProjectRepository point to the same Git repository (which itself wouldn’t be the end of the world, but this is an easy way to eliminate this risk).
118+
119+
To generate a list of all of the repositories a user can access, so that we can insert those values into an AppProject, one would do: ‘SELECT \* from AppProjectRepository WHERE user \= (user id)’
120+
121+
* As above, ensuring we have an index on user should keep this query efficient.
122+
123+
**New database table \- AppProjectManagedEnvironment:**
124+
125+
* ClusterUser
126+
* DB index on this field, to allow us to quickly retrieve all items for a particular user.
127+
* ManagedEnvironment ID (foreign key, non-nullable)
128+
129+
This row tracks which ManagedEnvironments (Argo CD cluster secrets) a user has access to.
130+
131+
As above, SELECTing on the ClusterUser field would allow us to generate the list of all of the Argo CD cluster secrets (corresponding to ManagedEnvironments), so that we can insert these values into the AppProject resource.
132+
133+
**New behaviour \- ManagedEnvironment, RepositoryCredential, and Application reconcilers:**
134+
135+
When reconciling a GitOpsDeploymentManagedEnvironment (in *sharedresourceloop\_managedenv.go*) or a GitOpsDeploymentRepositoryCredential (in *repocred\_reconciler.go*):
136+
137+
* When a ManagedEnvironment/RepoCred is created, ensure the corresponding *AppProjectRepository/AppProjectManagedEnvironment* is created, pointing to that ManagedEnv/RepoCred.
138+
* Likewise when a ManagedEnvironment/RepoCred is modified, or deleted, ensure the corresponding AppProject\* row is removed.
139+
140+
When reconciling an Application (in application\_event\_runner\_deployments.go):
141+
142+
* When an Application is created/modified, ensure there exists an AppProjectRepository for the source field of the repository URL.
143+
* Why? This handles the case where a user has created a GitOpsDeployment pointing to a public Git repository, and thus doesn’t need to create a GitOpsDeploymentRepositoryCredential for it (which is only required for private repos)
144+
145+
For example, when a user creates/modifies a GitOpsDeployment, it might look like this:
146+
147+
```yaml
148+
apiVersion: managed-gitops.redhat.com/v1alpha1
149+
kind: GitOpsDeployment
150+
151+
metadata:
152+
name: managed-environment-gitops-depl
153+
namespace: jane
154+
155+
spec:
156+
157+
source:
158+
repoURL: https://github.com/redhat-appstudio/managed-gitops
159+
path: resources/test-data/sample-gitops-repository/environments/overlays/dev
160+
161+
destination:
162+
# (...)
163+
```
164+
165+
The backend component will receive the create/modify event in application\_event\_runner\_deployments.go, and the event will point to the GitOpsDeployment by name/namespace.
166+
167+
* Normally, the next step once we receive the GitOpsDeployment event is to process it, which involves creating/modifying a row in the Application table: for example, converting the above into a new Application row, containing a spec\_field with the above contents.
168+
* But, new for this story is this: before we create the Application row, we should also first create an AppProjectRepository row containing these values:
169+
* **clusterUser:** clusteruser id based on the namespace that the GitOpsDeployment is in
170+
* **repositoryCredential:** empty (a “” string), in this case, because the this AppProjectRepository value is being generated based on an Application, and not from a RepositoryCredential
171+
* This field is nil (“”) if it was generated from a GitOpsDeployment, and should be a foreign key to a RepositoryCredential if it was generated from a RepositoryCredential.
172+
* **Normalized repository url:** https://github.com/redhat-appstudio/managed-gitops
173+
* This value is based on the .spec.repoURL field of the GitOpsDeployment
174+
* Next, after we continue as usual: we create/modify the Application row like we normally do in this file.
175+
176+
## Changes to cluster-agent component
177+
178+
**New behaviour \- when an Operation pointing to an Application is reconciled:**
179+
180+
When we process an Operation that points to an Application, before we generate (or update) the corresponding Argo CD Application, we should do the following:
181+
182+
* Generate the expected AppProject resource:
183+
* Get the ClusterUser from the operation\_owner\_user\_id
184+
* The name of the AppProject resource should be generated based on the clusteruser uid (something like e.g. ‘appproject-(clusteruser uid)’)
185+
* Select \* from AppProjectRepository
186+
* Select \* from AppProjectManagedEnvironment
187+
* Build an AppProject object using the above two values
188+
* Compare with AppProject in the namespace: ensure it matches the generated AppProject
189+
* If it matches, done.
190+
* If it doesn’t match, update it.
191+
* If it doesn’t exist, create it
192+
193+
And then, after we ensure the AppProject is consistent, when we generate the Argo CD Application, we should:
194+
195+
* (Always) ensure the ‘project’ field of the Application matches the ID of the AppProject
196+
* This includes ensuring that existing Applications are updated, if they previously has ‘project: default’
197+
198+
If an Application is deleted (e.g. we get an Operation that points to a deleted Application):
199+
200+
* For example, in ‘‘deleteArgoCDApplicationOfDeletedApplicationRow’
201+
* We should:
202+
* select count(\*) from AppProjectRepository where user \= (operation user id)
203+
* (and)
204+
* select count(\*) from AppProjectManagedEnvironment where user \= (operation user id)
205+
* This will tell us the number of these rows that exist for that user, in those 2 tables
206+
* If (\# of appprojectrepository for the user \+ \# of appprojectmanagedenvironment) \== 0, then delete the AppProject resource from the gitops-service-argocd namespace
207+
* Otherwise, don’t need the AppProject resource from the gitops-service-argocd namespace
208+
* Basically: the AppProject should be deleted if the user doesn’t have any GitOpsDeployments/GitOpsDeploymentRepositoryCredentials/GitOpsDeploymentManagedEnvironments defined in their API namespace, otherwise it should not be deleted.

0 commit comments

Comments
 (0)