Skip to content

Commit

Permalink
Retrieve project from Application
Browse files Browse the repository at this point in the history
Signed-off-by: Leonardo Luz Almeida <[email protected]>
  • Loading branch information
leoluz committed Aug 6, 2024
1 parent 504b65f commit 5180035
Show file tree
Hide file tree
Showing 12 changed files with 5,228 additions and 34 deletions.
30 changes: 28 additions & 2 deletions api/argoproj/v1alpha1/appproject.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,40 @@ type JWTToken struct {
ID string `json:"id,omitempty" protobuf:"bytes,3,opt,name=id"`
}

// AccessRequestList contains a list of AccessRequest
// AccessRequestList contains a list of AppProjects
// +kubebuilder:object:root=true
type AppProjectList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []AppProject `json:"items"`
}

// ApplicationSpec is a partial representation of the Argo CD Application
// resource.
// +kubebuilder:object:root=true
type Application struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
Spec ApplicationSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
}

// ApplicationSpec is a partial representation of the Argo CD ApplicationSpec
// resource. It just defines the project field which is an information required
// by ephemeral-access controller.
type ApplicationSpec struct {
// Project is a reference to the project this application belongs to.
// The empty string means that application belongs to the 'default' project.
Project string `json:"project" protobuf:"bytes,1,name=project"`
}

// ApplicationList contains a list of Applications
// +kubebuilder:object:root=true
type ApplicationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Application `json:"items"`
}

func init() {
SchemeBuilder.Register(&AppProject{}, &AppProjectList{})
SchemeBuilder.Register(&Application{}, &ApplicationList{}, &AppProject{}, &AppProjectList{})
}
73 changes: 73 additions & 0 deletions api/argoproj/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 10 additions & 9 deletions api/v1alpha1/accessrequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,19 @@ type AccessRequestSpec struct {
// TargetRoleName defines the role name the user will be assigned
// to once the access is approved
TargetRoleName string `json:"targetRoleName"`
// AppProject defines the Argo CD AppProject to assign the elevated
// Application defines the Argo CD Application to assign the elevated
// permission
AppProject TargetAppProject `json:"appProject"`
Application TargetApplication `json:"appProject"`
// Subjects defines the list of subjects for this access request
Subjects []Subject `json:"subjects"`
}

// TargetAppProject defines the Argo CD AppProject to assign the elevated
// TargetApplication defines the Argo CD AppProject to assign the elevated
// permission
type TargetAppProject struct {
// Name refers to the Argo CD AppProject name
type TargetApplication struct {
// Name refers to the Argo CD Application name
Name string `json:"name"`
// Namespace refers to the namespace where the Argo CD AppProject lives
// Namespace refers to the namespace where the Argo CD Application lives
Namespace string `json:"namespace"`
}

Expand All @@ -74,9 +74,10 @@ type Subject struct {

// AccessRequestStatus defines the observed state of AccessRequest
type AccessRequestStatus struct {
RequestState Status `json:"requestState,omitempty"`
ExpiresAt *metav1.Time `json:"expiresAt,omitempty"`
History []AccessRequestHistory `json:"history,omitempty"`
RequestState Status `json:"requestState,omitempty"`
TargetProject string `json:"targetProject,omitempty"`
ExpiresAt *metav1.Time `json:"expiresAt,omitempty"`
History []AccessRequestHistory `json:"history,omitempty"`
}

// AccessRequestHistory contain the history of all status transitions associated
Expand Down
10 changes: 5 additions & 5 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ spec:
properties:
appProject:
description: |-
AppProject defines the Argo CD AppProject to assign the elevated
Application defines the Argo CD Application to assign the elevated
permission
properties:
name:
description: Name refers to the Argo CD AppProject name
description: Name refers to the Argo CD Application name
type: string
namespace:
description: Namespace refers to the namespace where the Argo
CD AppProject lives
CD Application lives
type: string
required:
- name
Expand Down Expand Up @@ -133,6 +133,8 @@ spec:
- expired
- denied
type: string
targetProject:
type: string
type: object
type: object
served: true
Expand Down
6 changes: 6 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups:
- argoproj.io
resources:
- application
verbs:
- get
- apiGroups:
- argoproj.io
resources:
Expand Down
39 changes: 31 additions & 8 deletions internal/controller/accessrequest_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const (
// +kubebuilder:rbac:groups=ephemeral-access.argoproj-labs.io,resources=accessrequests/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=ephemeral-access.argoproj-labs.io,resources=accessrequests/finalizers,verbs=update
// +kubebuilder:rbac:groups=argoproj.io,resources=appproject,verbs=get;list;watch;update;patch
// +kubebuilder:rbac:groups=argoproj.io,resources=application,verbs=get

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
Expand Down Expand Up @@ -101,22 +102,29 @@ func (r *AccessRequestReconciler) Reconcile(ctx context.Context, req ctrl.Reques
}

logger.Debug("Validating AccessRequest")
// TODO implement a validation webhook to make fields immutable
err = ar.Validate()
if err != nil {
logger.Info("Validation error: %s", err)
return ctrl.Result{}, fmt.Errorf("error validating the AccessRequest: %w", err)
}

application, err := r.getApplication(ctx, ar)
if err != nil {
return ctrl.Result{}, fmt.Errorf("error getting Argo CD Application: %w", err)
}

// initialize the status if not done yet
if ar.Status.RequestState == "" {
logger.Debug("Initializing status")
ar.UpdateStatus(api.RequestedStatus, "")
ar.Status.TargetProject = application.Spec.Project
r.Status().Update(ctx, ar)
}

// check subject is sudoer
logger.Debug("Handling permission")
status, err := r.handlePermission(ctx, ar)
status, err := r.handlePermission(ctx, ar, application)
if err != nil {
logger.Error(err, "HandlePermission error")
return ctrl.Result{}, fmt.Errorf("error handling permission: %w", err)
Expand Down Expand Up @@ -164,7 +172,7 @@ func buildResult(status api.Status, ar *api.AccessRequest) ctrl.Result {
// it will return DeniedStatus.
//
// It will update the AccessRequest status accordingly with the situation.
func (r *AccessRequestReconciler) handlePermission(ctx context.Context, ar *api.AccessRequest) (api.Status, error) {
func (r *AccessRequestReconciler) handlePermission(ctx context.Context, ar *api.AccessRequest, app *argocd.Application) (api.Status, error) {
logger := log.FromContext(ctx)

if ar.IsExpiring() {
Expand All @@ -176,7 +184,7 @@ func (r *AccessRequestReconciler) handlePermission(ctx context.Context, ar *api.
return api.ExpiredStatus, nil
}

resp, err := r.Allowed(ctx, ar)
resp, err := r.Allowed(ctx, ar, app)
if err != nil {
return "", fmt.Errorf("error verifying if subject is allowed: %w", err)
}
Expand Down Expand Up @@ -229,6 +237,19 @@ func (r *AccessRequestReconciler) updateStatus(ctx context.Context, ar *api.Acce
return r.Status().Update(ctx, ar)
}

func (r *AccessRequestReconciler) getApplication(ctx context.Context, ar *api.AccessRequest) (*argocd.Application, error) {
application := &argocd.Application{}
objKey := client.ObjectKey{
Namespace: ar.Spec.Application.Namespace,
Name: ar.Spec.Application.Name,
}
err := r.Get(ctx, objKey, application)
if err != nil {
return nil, err
}
return application, nil
}

// removeArgoCDAccess will remove the subjects in the given AccessRequest from
// the given ar.TargetRoleName from the Argo CD project referenced in the
// ar.Spec.AppProject. The AppProject update will be executed via a patch with
Expand All @@ -237,10 +258,11 @@ func (r *AccessRequestReconciler) updateStatus(ctx context.Context, ar *api.Acce
func (r *AccessRequestReconciler) removeArgoCDAccess(ctx context.Context, ar *api.AccessRequest) error {
logger := log.FromContext(ctx)
logger.Info("Removing Argo CD Access")

project := &argocd.AppProject{}
objKey := client.ObjectKey{
Namespace: ar.Spec.AppProject.Namespace,
Name: ar.Spec.AppProject.Name,
Namespace: ar.Spec.Application.Namespace,
Name: ar.Status.TargetProject,
}
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
err := r.Get(ctx, objKey, project)
Expand All @@ -266,6 +288,7 @@ func (r *AccessRequestReconciler) removeArgoCDAccess(ctx context.Context, ar *ap
// removeSubjectsFromRole will iterate ovet the roles in the given project and
// remove the subjects from the given AccessRequest from the role specified in
// the ar.TargetRoleName.
// TODO revoke JWT tokens on every removal
func removeSubjectsFromRole(project *argocd.AppProject, ar *api.AccessRequest) {
for idx, role := range project.Spec.Roles {
if role.Name == ar.Spec.TargetRoleName {
Expand Down Expand Up @@ -297,8 +320,8 @@ func (r *AccessRequestReconciler) grantArgoCDAccess(ctx context.Context, ar *api
logger.Info("Granting Argo CD Access")
project := &argocd.AppProject{}
objKey := client.ObjectKey{
Namespace: ar.Spec.AppProject.Namespace,
Name: ar.Spec.AppProject.Name,
Namespace: ar.Spec.Application.Namespace,
Name: ar.Status.TargetProject,
}
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
err := r.Get(ctx, objKey, project)
Expand Down Expand Up @@ -384,7 +407,7 @@ type AllowedResponse struct {
// 0. implement the plugin system
// 1. verify if user is sudoer
// 2. verify if CR is approved
func (r *AccessRequestReconciler) Allowed(ctx context.Context, ar *api.AccessRequest) (AllowedResponse, error) {
func (r *AccessRequestReconciler) Allowed(ctx context.Context, ar *api.AccessRequest, app *argocd.Application) (AllowedResponse, error) {
return AllowedResponse{Allowed: true}, nil
}

Expand Down
Loading

0 comments on commit 5180035

Please sign in to comment.