Skip to content

Commit

Permalink
Add SourceGCPProject interface to obtain data owner and availability …
Browse files Browse the repository at this point in the history
…class
  • Loading branch information
grzr committed Apr 16, 2024
1 parent 6801e48 commit 44f193a
Show file tree
Hide file tree
Showing 19 changed files with 518 additions and 62 deletions.
73 changes: 66 additions & 7 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
- [Default](#default-2)
- [Principal Provider](#principal-provider)
- [Default](#default-3)
- [Source Project Provider](#source-project-provider)
- [Default](#default-4)
- [Internal Data Model and Backup Mechanics](#internal-data-model-and-backup-mechanics)

# Introduction
Expand Down Expand Up @@ -301,13 +303,40 @@ be impersonated. This is done by setting the `DEFAULT_PROVIDER_IMPERSONATE_GOOGL

## Principal Provider

The final provider provides the users principal. What is meant by user principal? Lets find out
by looking at the `Principal` data type. The Principal consist of the users email (which is a string) and a list
of role bindings. The role bindings, in turn, consist of project id and users role for this project. A user can have
one of three roles for a project `None`, `Viewer` or `Owner`. Let's take a step back and look what `PrincipalProvider`
actually does. It returns the users roles for each project. Why is this important? Because a user can only do a backup,
if he is the `Owner` of a project. Without the user has no right to edit any data of the project. The `PrincipalProvider`
interface consist of one method, which receives an email address and returns the users principal data.
This section explains the concept of a user principal and the role of the `PrincipalProvider` interface.

### User Principal:

* A `Principal` data type represents a user's identity and access rights within the system.
* It contains two components:
* `email`: A string representing the user's email address (unique identifier).
* `role_bindings`: A list of role bindings, which define a user's role within each project.

### Role Binding:

* A role binding associates a project ID with a user's role for that specific project.
* Possible roles are:
* `None`: User has no access to the project.
* `Viewer`: User can view project data but cannot modify it.
* `Owner`: User has full access to the project, including editing and backup privileges.

### PrincipalProvider Interface:

* The `PrincipalProvider interface defines a single method:
* `GetPrincipal(email: string) -> Principal`: This method takes a user's email address and returns their corresponding Principal data type.

### Importance of PrincipalProvider:

* The `PrincipalProvider` plays a crucial role in access control.
* By retrieving a user's principal data, the system can determine their roles for specific projects.
* This information is critical for authorizing actions:
* Only `Owner` users can perform backups.
* Users without the appropriate role (e.g., `None` or `Viewer`) cannot edit project data.

### In summary:

* The Principal data type stores user identity and project access levels.
* The PrincipalProvider interface provides access to this information for authorization purposes.


```go
Expand Down Expand Up @@ -376,6 +405,36 @@ The content can look like this.
project: 'project-two'
```

## Source Project Provider

source project orincipal is used to retrieve additioanl information about the source project. The `SourceGCPProjectProvider`
represents the interface for this provider. It contains only one method, which returns the `SourceGCPProject` for a given
project id.

```go
package provider
import (
"context"
)
type SourceGCPProjectProvider interface {
GetSourceGCPProject(ctxIn context.Context, gcpProjectID string) (SourceGCPProject, error)
}
```

### Default

Now let's have a look at the default implementation. The default is very similar to the `SinkGCPProjectProvider`. It
also needs the path to a `.yaml` file. Therefore `DEFAULT_BACKUP_SINK_PROVIDER_FOR_PROJECT_FILE_PATH` needs to be set.
The content can look like this.

```yaml
- project: local-account
availability_class: A1
data_owner: john.doe
```

# Internal Data Model and Backup Mechanics

Penelope tracks backup configuration specified by the user as well as the backups current success state in the `backups`
Expand Down
7 changes: 4 additions & 3 deletions cmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var envKeys = []config.EnvKey{

// AppStartArguments holds the necessary arguments to start the app
type AppStartArguments struct {
SourceGCPProjectProvider provider.SourceGCPProjectProvider
SinkGCPProjectProvider provider.SinkGCPProjectProvider
TargetPrincipalForProjectProvider impersonate.TargetPrincipalForProjectProvider
SecretProvider secret.SecretProvider
Expand Down Expand Up @@ -100,9 +101,9 @@ func validateEnvironmentVariables() {

func createBuilder(provider AppStartArguments) *builder.ProcessorBuilder {
return builder.NewProcessorBuilder(
processor.NewCreatingProcessorFactory(provider.SinkGCPProjectProvider, provider.TargetPrincipalForProjectProvider, provider.SecretProvider),
processor.NewGettingProcessorFactory(provider.TargetPrincipalForProjectProvider, provider.SecretProvider),
processor.NewListingProcessorFactory(provider.TargetPrincipalForProjectProvider, provider.SecretProvider),
processor.NewCreatingProcessorFactory(provider.SinkGCPProjectProvider, provider.TargetPrincipalForProjectProvider, provider.SecretProvider, provider.SourceGCPProjectProvider),
processor.NewGettingProcessorFactory(provider.TargetPrincipalForProjectProvider, provider.SecretProvider, provider.SourceGCPProjectProvider),
processor.NewListingProcessorFactory(provider.TargetPrincipalForProjectProvider, provider.SecretProvider, provider.SourceGCPProjectProvider),
processor.NewUpdatingProcessorFactory(provider.TargetPrincipalForProjectProvider, provider.SecretProvider),
processor.NewRestoringProcessorFactory(provider.TargetPrincipalForProjectProvider, provider.SecretProvider),
processor.NewCalculatingProcessorFactory(provider.SinkGCPProjectProvider, provider.TargetPrincipalForProjectProvider),
Expand Down
23 changes: 22 additions & 1 deletion frontend/src/components/BackupCreateDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import ComplianceCheck from "@/components/ComplianceCheck.vue";
import PricePrediction from "@/components/PricePrediction.vue";
import {capitalize} from "@/helpers/filters";
import {BackupStrategy, DefaultService} from "@/models/api";
import {BackupStrategy, DefaultService, SourceProject} from "@/models/api";
import {BackupType} from "@/models/api/models/BackupType";
import {CreateRequest} from "@/models/api/models/CreateRequest";
import Notification from "@/models/notification";
Expand All @@ -16,6 +16,7 @@ const model = defineModel<boolean>();
const isLoading = ref(true);
const sourceProjects = ref<string[]>([]);
const sourceProject = ref<SourceProject | undefined>(undefined);
const storageClasses = ref<{ title: string; value: string }[]>([]);
const storageRegions = ref<string[]>([]);
const backupTypes = ref([
Expand All @@ -24,6 +25,7 @@ const backupTypes = ref([
]);
const strategies = ref(Object.values(BackupStrategy));
const loadingSourceProject = ref(false);
const loadingBucketNames = ref(false);
const bucketNames = ref<string[]>([]);
const loadingDatasetNames = ref(false);
Expand Down Expand Up @@ -96,12 +98,27 @@ const updateDatasetNames = () => {
}
};
function updateSourceProject() {
if (request.value.project) {
loadingSourceProject.value = true;
DefaultService.getSourceProject(request.value.project)
.then((response) => {
sourceProject.value = response.sourceProject;
})
.catch((err) => notificationsStore.handleError(err))
.finally(() => {
loadingSourceProject.value = false;
});
}
}
const updateSourceFields = () => {
if (request.value.type == BackupType.CLOUD_STORAGE) {
updateBucketNames();
} else if (request.value.type == BackupType.BIG_QUERY) {
updateDatasetNames();
}
updateSourceProject();
};
const apiRequestBody = () => {
Expand Down Expand Up @@ -187,6 +204,10 @@ watch(
@update:model-value="updateSourceFields()"
:rules="[requiredRule('Project')]"
></v-select>
<v-text-field v-if="sourceProject?.data_owner" label="Data owner" v-model="sourceProject.data_owner"
readonly></v-text-field>
<v-text-field v-if="sourceProject?.availability_class" label="Availability class" v-model="sourceProject.availability_class"
readonly></v-text-field>
<v-select
label="Backup type*"
:items="backupTypes"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/models/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type { RecoveryTimeObjective } from './models/RecoveryTimeObjective';
export type { RestoreResponse } from './models/RestoreResponse';
export { Role } from './models/Role';
export type { SnapshotOptions } from './models/SnapshotOptions';
export type { SourceProject } from './models/SourceProject';
export type { TargetOptions } from './models/TargetOptions';
export type { UpdateRequest } from './models/UpdateRequest';
export type { UserResponse } from './models/UserResponse';
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/models/api/models/SourceProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

import type { AvailabilityClass } from './AvailabilityClass';

export type SourceProject = {
data_owner?: string;
availability_class?: AvailabilityClass;
};

24 changes: 24 additions & 0 deletions frontend/src/models/api/services/DefaultService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { GCSOptions } from '../models/GCSOptions';
import type { MirrorOptions } from '../models/MirrorOptions';
import type { RestoreResponse } from '../models/RestoreResponse';
import type { SnapshotOptions } from '../models/SnapshotOptions';
import type { SourceProject } from '../models/SourceProject';
import type { TargetOptions } from '../models/TargetOptions';
import type { UpdateRequest } from '../models/UpdateRequest';
import type { UserResponse } from '../models/UserResponse';
Expand Down Expand Up @@ -305,6 +306,29 @@ export class DefaultService {
});
}

/**
* Get source project
* @param projectId Project ID
* @returns any OK
* @throws ApiError
*/
public static getSourceProject(
projectId: string,
): CancelablePromise<{
sourceProject?: SourceProject;
}> {
return __request(OpenAPI, {
method: 'GET',
url: '/sourceProject/{projectId}',
path: {
'projectId': projectId,
},
errors: {
400: `Bad Request`,
},
});
}

/**
* Run a task
* @param task Task name
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const (
SetTestUser EnvKey = "SET_TEST_USER"
IsProviderLocal EnvKey = "IS_PROVIDER_LOCAL"
DefaultProviderBucketEnv EnvKey = "DEFAULT_PROVIDER_BUCKET"
DefaultProvidersCacheTTLEnv EnvKey = "DEFAULT_PROVIDER_CACHE_TTL" // in minutes
DefaultProviderGCPSourceProjectPathEnv EnvKey = "DEFAULT_GCP_SOURCE_PROJECT_PROVIDER_FILE_PATH"
DefaultProviderSinkForProjectPathEnv EnvKey = "DEFAULT_BACKUP_SINK_PROVIDER_FOR_PROJECT_FILE_PATH"
DefaultProviderPrincipalForUserPathEnv EnvKey = "DEFAULT_USER_PRINCIPAL_PROVIDER_FILE_PATH"
DefaultProviderImpersonateGoogleServiceAccountEnv EnvKey = "DEFAULT_PROVIDER_IMPERSONATE_GOOGLE_SERVICE_ACCOUNT"
Expand Down
2 changes: 1 addition & 1 deletion pkg/http/auth/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func CheckRequestIsAllowed(principal *model.Principal, requestType requestobject
case requestobjects.Creating:
isAllowed = matchRole(rbacRole, model.Owner)
case requestobjects.Getting, requestobjects.Listing, requestobjects.Restoring, requestobjects.Calculating,
requestobjects.DatasetListing, requestobjects.BucketListing:
requestobjects.DatasetListing, requestobjects.BucketListing, requestobjects.SourceProjectGet:
isAllowed = matchRole(rbacRole, model.Owner, model.Viewer)
}

Expand Down
42 changes: 25 additions & 17 deletions pkg/processor/creating_processor_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ type CreatingProcessorFactory interface {

// creatingProcessorFactory create Process for Creating
type creatingProcessorFactory struct {
backupProvider provider.SinkGCPProjectProvider
tokenSourceProvider impersonate.TargetPrincipalForProjectProvider
credentialsProvider secret.SecretProvider
backupProvider provider.SinkGCPProjectProvider
tokenSourceProvider impersonate.TargetPrincipalForProjectProvider
credentialsProvider secret.SecretProvider
sourceGCPProjectProvider provider.SourceGCPProjectProvider
}

func NewCreatingProcessorFactory(backupProvider provider.SinkGCPProjectProvider, tokenSourceProvider impersonate.TargetPrincipalForProjectProvider, credentialsProvider secret.SecretProvider) CreatingProcessorFactory {
func NewCreatingProcessorFactory(backupProvider provider.SinkGCPProjectProvider, tokenSourceProvider impersonate.TargetPrincipalForProjectProvider, credentialsProvider secret.SecretProvider, sourceGCPProjectProvider provider.SourceGCPProjectProvider) CreatingProcessorFactory {
return &creatingProcessorFactory{
backupProvider: backupProvider,
tokenSourceProvider: tokenSourceProvider,
credentialsProvider: credentialsProvider,
backupProvider: backupProvider,
tokenSourceProvider: tokenSourceProvider,
credentialsProvider: credentialsProvider,
sourceGCPProjectProvider: sourceGCPProjectProvider,
}
}

Expand All @@ -56,18 +58,20 @@ func (c *creatingProcessorFactory) CreateProcessor(ctxIn context.Context) (Opera
}

return &creatingProcessor{
BackupRepository: backupRepository,
JobRepository: jobRepository,
backupProvider: c.backupProvider,
tokenSourceProvider: c.tokenSourceProvider,
BackupRepository: backupRepository,
JobRepository: jobRepository,
backupProvider: c.backupProvider,
tokenSourceProvider: c.tokenSourceProvider,
sourceGCPProjectProvider: c.sourceGCPProjectProvider,
}, nil
}

type creatingProcessor struct {
BackupRepository repository.BackupRepository
JobRepository repository.JobRepository
backupProvider provider.SinkGCPProjectProvider
tokenSourceProvider impersonate.TargetPrincipalForProjectProvider
BackupRepository repository.BackupRepository
JobRepository repository.JobRepository
backupProvider provider.SinkGCPProjectProvider
sourceGCPProjectProvider provider.SourceGCPProjectProvider
tokenSourceProvider impersonate.TargetPrincipalForProjectProvider
}

func (b *creatingProcessor) Process(ctxIn context.Context, args *Argument[requestobjects.CreateRequest]) (requestobjects.BackupResponse, error) {
Expand All @@ -77,7 +81,7 @@ func (b *creatingProcessor) Process(ctxIn context.Context, args *Argument[reques
var request requestobjects.CreateRequest = args.Request

if !auth.CheckRequestIsAllowed(args.Principal, requestobjects.Creating, request.Project) {
return requestobjects.BackupResponse{}, fmt.Errorf("%s is not allowed for user %q on project %q", requestobjects.Creating.String(), args.Principal.User.Email, request.Project)
return requestobjects.BackupResponse{}, fmt.Errorf("%sourceGCPProject is not allowed for user %q on project %q", requestobjects.Creating.String(), args.Principal.User.Email, request.Project)
}

backup, err := b.prepareBackupFromRequest(ctx, request)
Expand All @@ -103,7 +107,11 @@ func (b *creatingProcessor) Process(ctxIn context.Context, args *Argument[reques
if err != nil {
return requestobjects.BackupResponse{}, err
}
return mapBackupToResponse(processedBackup, nil), nil
sourceGCPProject, err := b.sourceGCPProjectProvider.GetSourceGCPProject(ctx, request.Project)
if err != nil {
return requestobjects.BackupResponse{}, err
}
return mapBackupToResponse(processedBackup, nil, sourceGCPProject), nil
}

func (b *creatingProcessor) prepareBackupFromRequest(ctxIn context.Context, request requestobjects.CreateRequest) (*repository.Backup, error) {
Expand Down
23 changes: 15 additions & 8 deletions pkg/processor/getting_processor_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package processor
import (
"context"
"fmt"
"github.com/ottogroup/penelope/pkg/provider"

"github.com/go-pg/pg/v10"
"github.com/golang/glog"
Expand All @@ -21,12 +22,13 @@ type GettingProcessorFactory interface {

// GettingProcessorFactory create Process for Getting
type gettingProcessorFactory struct {
tokenSourceProvider impersonate.TargetPrincipalForProjectProvider
credentialProvider secret.SecretProvider
tokenSourceProvider impersonate.TargetPrincipalForProjectProvider
credentialProvider secret.SecretProvider
sourceGCPProjectProvider provider.SourceGCPProjectProvider
}

func NewGettingProcessorFactory(tokenSourceProvider impersonate.TargetPrincipalForProjectProvider, credentialProvider secret.SecretProvider) GettingProcessorFactory {
return &gettingProcessorFactory{tokenSourceProvider, credentialProvider}
func NewGettingProcessorFactory(tokenSourceProvider impersonate.TargetPrincipalForProjectProvider, credentialProvider secret.SecretProvider, sourceGCPProjectProvider provider.SourceGCPProjectProvider) GettingProcessorFactory {
return &gettingProcessorFactory{tokenSourceProvider, credentialProvider, sourceGCPProjectProvider}
}

// CreateProcessor return instance of Operations for Getting
Expand All @@ -45,12 +47,13 @@ func (c gettingProcessorFactory) CreateProcessor(ctxIn context.Context) (Operati
return &gettingProcessor{}, err
}

return &gettingProcessor{BackupRepository: backupRepository, JobRepository: jobRepository}, nil
return &gettingProcessor{BackupRepository: backupRepository, JobRepository: jobRepository, sourceGCPProjectProvider: c.sourceGCPProjectProvider}, nil
}

type gettingProcessor struct {
BackupRepository repository.BackupRepository
JobRepository repository.JobRepository
BackupRepository repository.BackupRepository
JobRepository repository.JobRepository
sourceGCPProjectProvider provider.SourceGCPProjectProvider
}

// Process request
Expand Down Expand Up @@ -94,8 +97,12 @@ func (l gettingProcessor) Process(ctxIn context.Context, args *Argument[requesto
for _, status := range repository.JobStatutses {
countedJobs += jobsStats[status]
}
sourceProject, err := l.sourceGCPProjectProvider.GetSourceGCPProject(ctx, backup.SourceProject)
if err != nil {
return requestobjects.BackupResponse{}, errors.Wrapf(err, "sourceGCPProjectProvider GetSourceGCPProject failed %s", backup.SourceProject)
}

res := mapBackupToResponse(backup, jobs)
res := mapBackupToResponse(backup, jobs, sourceProject)
res.JobsTotal = countedJobs
return res, err
}
Loading

0 comments on commit 44f193a

Please sign in to comment.