diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 8dec3f9..d885cdc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -63,4 +63,12 @@ rules: - get - patch - update +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch diff --git a/controllers/imagerepository_controller.go b/controllers/imagerepository_controller.go index 726132c..83bc4ff 100644 --- a/controllers/imagerepository_controller.go +++ b/controllers/imagerepository_controller.go @@ -49,6 +49,8 @@ const ( buildPipelineServiceAccountName = "appstudio-pipeline" updateComponentAnnotationName = "image-controller.appstudio.redhat.com/update-component-image" + additionalUsersConfigMapName = "image-controller-additional-users" + additionalUsersConfigMapKey = "quay.io" ) // ImageRepositoryReconciler reconciles a ImageRepository object @@ -79,6 +81,7 @@ func setMetricsTime(idForMetrics string, reconcileStartTime time.Time) { //+kubebuilder:rbac:groups=appstudio.redhat.com,resources=imagerepositories/status,verbs=get;update;patch //+kubebuilder:rbac:groups=appstudio.redhat.com,resources=imagerepositories/finalizers,verbs=update //+kubebuilder:rbac:groups=appstudio.redhat.com,resources=components,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch //+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;update;patch @@ -343,6 +346,11 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepository(ctx context.Context } } + err = r.GrantAdditionalRepositoryAccess(ctx, imageRepository) + if err != nil { + return err + } + var notificationStatus []imagerepositoryv1alpha1.NotificationStatus if notificationStatus, err = r.SetNotifications(ctx, imageRepository); err != nil { return err @@ -411,7 +419,7 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepositoryAccess(ctx context.C return nil, err } - err = r.QuayClient.AddPermissionsForRepositoryToRobotAccount(r.QuayOrganization, imageRepositoryName, robotAccount.Name, !isPullOnly) + err = r.QuayClient.AddPermissionsForRepositoryToAccount(r.QuayOrganization, imageRepositoryName, robotAccount.Name, true, !isPullOnly) if err != nil { log.Error(err, "failed to add permissions to robot account", "RobotAccountName", robotAccountName, l.Action, l.ActionUpdate, l.Audit, "true") return nil, err @@ -429,6 +437,51 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepositoryAccess(ctx context.C return data, nil } +func (r *ImageRepositoryReconciler) GrantAdditionalRepositoryAccess(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository) error { + log := ctrllog.FromContext(ctx).WithName("GrantAdditionalRepositoryAccess") + ctx = ctrllog.IntoContext(ctx, log) + + additionalUsersConfigMap := &corev1.ConfigMap{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: additionalUsersConfigMapName, Namespace: imageRepository.Namespace}, additionalUsersConfigMap); err != nil { + if errors.IsNotFound(err) { + log.Info("Config map with additional users doesn't exist", "ConfigMapName", additionalUsersConfigMapName, l.Action, l.ActionView) + return nil + } + log.Error(err, "failed to read config map with additional users", "ConfigMapName", additionalUsersConfigMapName, l.Action, l.ActionView) + return err + } + additionalUsersStr, usersExist := additionalUsersConfigMap.Data[additionalUsersConfigMapKey] + if !usersExist { + log.Info("Config map with additional users doesn't have the key", "ConfigMapName", additionalUsersConfigMapName, "ConfigMapKey", additionalUsersConfigMapKey, l.Action, l.ActionView) + return nil + } + + additionalUsers := strings.Fields(strings.TrimSpace(additionalUsersStr)) + log.Info("Additional users configured in config map", "AdditionalUsers", additionalUsers) + + imageRepositoryName := imageRepository.Spec.Image.Name + + for _, user := range additionalUsers { + err := r.QuayClient.AddPermissionsForRepositoryToAccount(r.QuayOrganization, imageRepositoryName, user, false, false) + if err != nil { + if strings.Contains(err.Error(), "Invalid username:") { + log.Info("failed to add permissions for account, because it doesn't exist", "AccountName", user) + continue + } + + log.Error(err, "failed to add permissions for account", "AccountName", user, l.Action, l.ActionUpdate, l.Audit, "true") + return err + } + log.Info("Additional user access was granted for", "UserName", user) + } + listPerms, _ := r.QuayClient.ListPermissionsForRepository(r.QuayOrganization, imageRepositoryName) + log.Info("Repository has now following permissions", "RepositoryName", imageRepositoryName) + for _, user := range listPerms { + log.Info(fmt.Sprintf("name: %s; role: %s; is robot: %t; is org member: %t", user.Name, user.Role, user.IsRobot, user.IsOrgMember)) + } + return nil +} + // RegenerateImageRepositoryCredentials rotates robot account(s) token and updates corresponding secret(s) func (r *ImageRepositoryReconciler) RegenerateImageRepositoryCredentials(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository) error { log := ctrllog.FromContext(ctx) diff --git a/controllers/imagerepository_controller_test.go b/controllers/imagerepository_controller_test.go index 69db2da..295566e 100644 --- a/controllers/imagerepository_controller_test.go +++ b/controllers/imagerepository_controller_test.go @@ -83,14 +83,14 @@ var _ = Describe("Image repository controller", func() { Expect(strings.HasPrefix(robotName, expectedRobotAccountPrefix)).To(BeTrue()) return &quay.RobotAccount{Name: robotName, Token: pushToken}, nil } - isAddPushPermissionsToRobotAccountInvoked := false - quay.AddPermissionsForRepositoryToRobotAccountFunc = func(organization, imageRepository, robotAccountName string, isWrite bool) error { + isAddPushPermissionsToAccountInvoked := false + quay.AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { defer GinkgoRecover() - isAddPushPermissionsToRobotAccountInvoked = true + isAddPushPermissionsToAccountInvoked = true Expect(organization).To(Equal(quay.TestQuayOrg)) Expect(imageRepository).To(Equal(expectedImageName)) Expect(isWrite).To(BeTrue()) - Expect(strings.HasPrefix(robotAccountName, expectedRobotAccountPrefix)).To(BeTrue()) + Expect(strings.HasPrefix(accountName, expectedRobotAccountPrefix)).To(BeTrue()) return nil } @@ -105,7 +105,7 @@ var _ = Describe("Image repository controller", func() { Eventually(func() bool { return isCreateRepositoryInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreateRobotAccountInvoked }, timeout, interval).Should(BeTrue()) - Eventually(func() bool { return isAddPushPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue()) + Eventually(func() bool { return isAddPushPermissionsToAccountInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreateNotificationInvoked }, timeout, interval).Should(BeFalse()) waitImageRepositoryFinalizerOnImageRepository(resourceKey) @@ -356,14 +356,14 @@ var _ = Describe("Image repository controller", func() { Expect(strings.HasPrefix(robotName, expectedRobotAccountPrefix)).To(BeTrue()) return &quay.RobotAccount{Name: robotName, Token: pushToken}, nil } - isAddPushPermissionsToRobotAccountInvoked := false - quay.AddPermissionsForRepositoryToRobotAccountFunc = func(organization, imageRepository, robotAccountName string, isWrite bool) error { + isAddPushPermissionsToAccountInvoked := false + quay.AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { defer GinkgoRecover() - isAddPushPermissionsToRobotAccountInvoked = true + isAddPushPermissionsToAccountInvoked = true Expect(organization).To(Equal(quay.TestQuayOrg)) Expect(imageRepository).To(Equal(expectedImageName)) Expect(isWrite).To(BeTrue()) - Expect(strings.HasPrefix(robotAccountName, expectedRobotAccountPrefix)).To(BeTrue()) + Expect(strings.HasPrefix(accountName, expectedRobotAccountPrefix)).To(BeTrue()) return nil } @@ -377,7 +377,7 @@ var _ = Describe("Image repository controller", func() { Eventually(func() bool { return isCreateRepositoryInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreateRobotAccountInvoked }, timeout, interval).Should(BeTrue()) - Eventually(func() bool { return isAddPushPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue()) + Eventually(func() bool { return isAddPushPermissionsToAccountInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreateNotificationInvoked }, timeout, interval).Should(BeFalse()) waitImageRepositoryFinalizerOnImageRepository(resourceKey) @@ -500,19 +500,19 @@ var _ = Describe("Image repository controller", func() { isCreatePushRobotAccountInvoked = true return &quay.RobotAccount{Name: robotName, Token: pushToken}, nil } - isAddPushPermissionsToRobotAccountInvoked := false - isAddPullPermissionsToRobotAccountInvoked := false - quay.AddPermissionsForRepositoryToRobotAccountFunc = func(organization, imageRepository, robotAccountName string, isWrite bool) error { + isAddPushPermissionsToAccountInvoked := false + isAddPullPermissionsToAccountInvoked := false + quay.AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { defer GinkgoRecover() Expect(organization).To(Equal(quay.TestQuayOrg)) Expect(imageRepository).To(Equal(expectedImageName)) - Expect(strings.HasPrefix(robotAccountName, expectedRobotAccountPrefix)).To(BeTrue()) - if strings.HasSuffix(robotAccountName, "_pull") { + Expect(strings.HasPrefix(accountName, expectedRobotAccountPrefix)).To(BeTrue()) + if strings.HasSuffix(accountName, "_pull") { Expect(isWrite).To(BeFalse()) - isAddPullPermissionsToRobotAccountInvoked = true + isAddPullPermissionsToAccountInvoked = true } else { Expect(isWrite).To(BeTrue()) - isAddPushPermissionsToRobotAccountInvoked = true + isAddPushPermissionsToAccountInvoked = true } return nil } @@ -565,8 +565,8 @@ var _ = Describe("Image repository controller", func() { Eventually(func() bool { return isCreateRepositoryInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreatePushRobotAccountInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreatePullRobotAccountInvoked }, timeout, interval).Should(BeTrue()) - Eventually(func() bool { return isAddPushPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue()) - Eventually(func() bool { return isAddPullPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue()) + Eventually(func() bool { return isAddPushPermissionsToAccountInvoked }, timeout, interval).Should(BeTrue()) + Eventually(func() bool { return isAddPullPermissionsToAccountInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreateNotificationInvoked }, timeout, interval).Should(BeTrue()) waitImageRepositoryFinalizerOnImageRepository(resourceKey) diff --git a/pkg/quay/api.go b/pkg/quay/api.go index afe2df4..a12b66b 100644 --- a/pkg/quay/api.go +++ b/pkg/quay/api.go @@ -44,6 +44,13 @@ type RobotAccount struct { Message string `json:"message"` } +type UserAccount struct { + Name string `json:"name"` + Role string `json:"role"` + IsRobot bool `json:is_robot` + IsOrgMember bool `json:is_org_member` +} + // Quay API can sometimes return {"error": "..."} and sometimes {"error_message": "..."} without the field error // In some cases the error is send alongside the response in the {"message": "..."} field. type QuayError struct { diff --git a/pkg/quay/quay.go b/pkg/quay/quay.go index a7c656d..8e43b65 100644 --- a/pkg/quay/quay.go +++ b/pkg/quay/quay.go @@ -34,7 +34,8 @@ type QuayService interface { GetRobotAccount(organization string, robotName string) (*RobotAccount, error) CreateRobotAccount(organization string, robotName string) (*RobotAccount, error) DeleteRobotAccount(organization string, robotName string) (bool, error) - AddPermissionsForRepositoryToRobotAccount(organization, imageRepository, robotAccountName string, isWrite bool) error + AddPermissionsForRepositoryToAccount(organization, imageRepository, accountName string, isRobot, isWrite bool) error + ListPermissionsForRepository(organization, imageRepository string) (map[string]UserAccount, error) RegenerateRobotAccountToken(organization string, robotName string) (*RobotAccount, error) GetAllRepositories(organization string) ([]Repository, error) GetAllRobotAccounts(organization string) ([]RobotAccount, error) @@ -359,18 +360,46 @@ func (c *QuayClient) DeleteRobotAccount(organization string, robotName string) ( return false, errors.New(data.ErrorMessage) } -// AddPermissionsForRepositoryToRobotAccount allows given robot account to access to the given repository. +// ListPermissionsForRepository list permissions for the given repository. +func (c *QuayClient) ListPermissionsForRepository(organization, imageRepository string) (map[string]UserAccount, error) { + url := fmt.Sprintf("%s/repository/%s/%s/permissions/user/", c.url, organization, imageRepository) + + resp, err := c.doRequest(url, http.MethodGet, nil) + + if err != nil { + return nil, fmt.Errorf("failed to Do request, error: %s", err) + } + if resp.GetStatusCode() != 200 { + return nil, fmt.Errorf("error getting permissions, got status code %d", resp.GetStatusCode()) + } + + type Response struct { + Permissions map[string]UserAccount `json:"permissions"` + } + var response Response + if err := resp.GetJson(&response); err != nil { + return nil, err + } + + return response.Permissions, nil +} + +// AddPermissionsForRepositoryToAccount allows given robot account to access to the given repository. // If isWrite is true, then pull and push permissions are added, otherwise - pull access only. -func (c *QuayClient) AddPermissionsForRepositoryToRobotAccount(organization, imageRepository, robotAccountName string, isWrite bool) error { - var robotAccountFullName string - if robotName, err := handleRobotName(robotAccountName); err == nil { - robotAccountFullName = organization + "+" + robotName +func (c *QuayClient) AddPermissionsForRepositoryToAccount(organization, imageRepository, accountName string, isRobot, isWrite bool) error { + var accountFullName string + if isRobot { + if robotName, err := handleRobotName(accountName); err == nil { + accountFullName = organization + "+" + robotName + } else { + return err + } } else { - return err + accountFullName = accountName } // url := "https://quay.io/api/v1/repository/redhat-appstudio/test-repo-using-api/permissions/user/redhat-appstudio+createdbysbose" - url := fmt.Sprintf("%s/repository/%s/%s/permissions/user/%s", c.url, organization, imageRepository, robotAccountFullName) + url := fmt.Sprintf("%s/repository/%s/%s/permissions/user/%s", c.url, organization, imageRepository, accountFullName) role := "read" if isWrite { @@ -392,7 +421,7 @@ func (c *QuayClient) AddPermissionsForRepositoryToRobotAccount(organization, ima message = data.Error } } - return fmt.Errorf("failed to add permissions to the robot account. Status code: %d, message: %s", resp.GetStatusCode(), message) + return fmt.Errorf("failed to add permissions to the account: %s. Status code: %d, message: %s", accountFullName, resp.GetStatusCode(), message) } return nil } diff --git a/pkg/quay/quay_debug_test.go b/pkg/quay/quay_debug_test.go index 7cf7ac5..9e79c90 100644 --- a/pkg/quay/quay_debug_test.go +++ b/pkg/quay/quay_debug_test.go @@ -172,7 +172,7 @@ func TestAddPermissionsToRobotAccount(t *testing.T) { } quayClient := NewQuayClient(&http.Client{Transport: &http.Transport{}}, quayToken, quayApiUrl) - err := quayClient.AddPermissionsForRepositoryToRobotAccount(quayOrgName, quayImageRepoName, quayRobotAccountName, true) + err := quayClient.AddPermissionsForRepositoryToAccount(quayOrgName, quayImageRepoName, quayRobotAccountName, true, true) if err != nil { t.Fatal(err) } diff --git a/pkg/quay/quay_test.go b/pkg/quay/quay_test.go index 18ad23c..47c176c 100644 --- a/pkg/quay/quay_test.go +++ b/pkg/quay/quay_test.go @@ -335,7 +335,7 @@ func TestQuayClient_AddPermissions(t *testing.T) { robotName: robotName, statusCode: 400, responseData: "{\"name: \"info\"}", - expectedErr: "failed to add permissions to the robot account", + expectedErr: "failed to add permissions to the account", }, { name: "stop if http request fails", @@ -357,7 +357,7 @@ func TestQuayClient_AddPermissions(t *testing.T) { } quayClient := NewQuayClient(client, "authtoken", testQuayApiUrl) - err := quayClient.AddPermissionsForRepositoryToRobotAccount("org", "repository", tc.robotName, true) + err := quayClient.AddPermissionsForRepositoryToAccount("org", "repository", tc.robotName, true, true) if tc.expectedErr == "" { assert.NilError(t, err) diff --git a/pkg/quay/test_quay_client.go b/pkg/quay/test_quay_client.go index a7331e6..9ba4f98 100644 --- a/pkg/quay/test_quay_client.go +++ b/pkg/quay/test_quay_client.go @@ -30,18 +30,19 @@ type TestQuayClient struct{} var _ QuayService = (*TestQuayClient)(nil) var ( - CreateRepositoryFunc func(repository RepositoryRequest) (*Repository, error) - DeleteRepositoryFunc func(organization, imageRepository string) (bool, error) - ChangeRepositoryVisibilityFunc func(organization, imageRepository string, visibility string) error - GetRobotAccountFunc func(organization string, robotName string) (*RobotAccount, error) - CreateRobotAccountFunc func(organization string, robotName string) (*RobotAccount, error) - DeleteRobotAccountFunc func(organization string, robotName string) (bool, error) - AddPermissionsForRepositoryToRobotAccountFunc func(organization, imageRepository, robotAccountName string, isWrite bool) error - RegenerateRobotAccountTokenFunc func(organization string, robotName string) (*RobotAccount, error) - GetNotificationsFunc func(organization, repository string) ([]Notification, error) - CreateNotificationFunc func(organization, repository string, notification Notification) (*Notification, error) - UpdateNotificationFunc func(organization, repository string, notificationUuid string, notification Notification) (*Notification, error) - DeleteNotificationFunc func(organization, repository string, notificationUuid string) (bool, error) + CreateRepositoryFunc func(repository RepositoryRequest) (*Repository, error) + DeleteRepositoryFunc func(organization, imageRepository string) (bool, error) + ChangeRepositoryVisibilityFunc func(organization, imageRepository string, visibility string) error + GetRobotAccountFunc func(organization string, robotName string) (*RobotAccount, error) + CreateRobotAccountFunc func(organization string, robotName string) (*RobotAccount, error) + DeleteRobotAccountFunc func(organization string, robotName string) (bool, error) + AddPermissionsForRepositoryToAccountFunc func(organization, imageRepository, accountName string, isRobot, isWrite bool) error + ListPermissionsForRepositoryFunc func(organization, imageRepository string) (map[string]UserAccount, error) + RegenerateRobotAccountTokenFunc func(organization string, robotName string) (*RobotAccount, error) + GetNotificationsFunc func(organization, repository string) ([]Notification, error) + CreateNotificationFunc func(organization, repository string, notification Notification) (*Notification, error) + UpdateNotificationFunc func(organization, repository string, notificationUuid string, notification Notification) (*Notification, error) + DeleteNotificationFunc func(organization, repository string, notificationUuid string) (bool, error) ) func ResetTestQuayClient() { @@ -51,7 +52,8 @@ func ResetTestQuayClient() { GetRobotAccountFunc = func(organization, robotName string) (*RobotAccount, error) { return &RobotAccount{}, nil } CreateRobotAccountFunc = func(organization, robotName string) (*RobotAccount, error) { return &RobotAccount{}, nil } DeleteRobotAccountFunc = func(organization, robotName string) (bool, error) { return true, nil } - AddPermissionsForRepositoryToRobotAccountFunc = func(organization, imageRepository, robotAccountName string, isWrite bool) error { return nil } + AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { return nil } + ListPermissionsForRepositoryFunc = func(organization, imageRepository string) (map[string]UserAccount, error) {return nil, nil} RegenerateRobotAccountTokenFunc = func(organization, robotName string) (*RobotAccount, error) { return &RobotAccount{}, nil } GetNotificationsFunc = func(organization, repository string) ([]Notification, error) { return []Notification{}, nil } CreateNotificationFunc = func(organization, repository string, notification Notification) (*Notification, error) { @@ -96,11 +98,16 @@ func ResetTestQuayClientToFails() { Fail("DeleteRobotAccount invoked") return true, nil } - AddPermissionsForRepositoryToRobotAccountFunc = func(organization, imageRepository, robotAccountName string, isWrite bool) error { + AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { defer GinkgoRecover() - Fail("AddPermissionsForRepositoryToRobotAccount invoked") + Fail("AddPermissionsForRepositoryToAccount invoked") return nil } + ListPermissionsForRepositoryFunc = func(organization, imageRepository string) (map[string]UserAccount, error) { + defer GinkgoRecover() + Fail("ListPermissionsForRepository invoked") + return nil, nil + } RegenerateRobotAccountTokenFunc = func(organization, robotName string) (*RobotAccount, error) { defer GinkgoRecover() Fail("RegenerateRobotAccountToken invoked") @@ -146,8 +153,11 @@ func (c TestQuayClient) CreateRobotAccount(organization string, robotName string func (c TestQuayClient) DeleteRobotAccount(organization string, robotName string) (bool, error) { return DeleteRobotAccountFunc(organization, robotName) } -func (c TestQuayClient) AddPermissionsForRepositoryToRobotAccount(organization, imageRepository, robotAccountName string, isWrite bool) error { - return AddPermissionsForRepositoryToRobotAccountFunc(organization, imageRepository, robotAccountName, isWrite) +func (c TestQuayClient) AddPermissionsForRepositoryToAccount(organization, imageRepository, accountName string, isRobot, isWrite bool) error { + return AddPermissionsForRepositoryToAccountFunc(organization, imageRepository, accountName, isRobot, isWrite) +} +func (c TestQuayClient) ListPermissionsForRepository(organization, imageRepository string) (map[string]UserAccount, error) { + return ListPermissionsForRepositoryFunc(organization, imageRepository) } func (c TestQuayClient) RegenerateRobotAccountToken(organization string, robotName string) (*RobotAccount, error) { return RegenerateRobotAccountTokenFunc(organization, robotName)