Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ buildifier(

genrule(
name = "copy_piped",
srcs = ["//cmd/piped"], #keep
srcs = ["//cmd/piped"],
outs = ["piped"],
cmd = "cp $< $@",
)

# gazelle:exclude pkg/app/helloworld/service/service.pb.validate.go
# gazelle:exclude pkg/app/api/service/webservice/service.pb.validate.go
# gazelle:exclude pkg/app/api/service/pipedservice/service.pb.validate.go
# gazelle:exclude pkg/model/apikey.pb.validate.go
# gazelle:exclude pkg/model/application.pb.validate.go
# gazelle:exclude pkg/model/application_live_state.pb.validate.go
# gazelle:exclude pkg/model/command.pb.validate.go
Expand All @@ -57,4 +58,4 @@ genrule(
# gazelle:exclude pkg/model/role.pb.validate.go
# gazelle:exclude pkg/model/user.pb.validate.go
# gazelle:exclude pkg/app/ops/handler/templates.embed.go
# gazelle:exclude pkg/app/api/api/deployment_config_templates.embed.go
# gazelle:exclude pkg/app/api/grpcapi/deployment_config_templates.embed.go
101 changes: 98 additions & 3 deletions pkg/app/api/grpcapi/web_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type WebAPI struct {
deploymentStore datastore.DeploymentStore
pipedStore datastore.PipedStore
projectStore datastore.ProjectStore
apiKeyStore datastore.APIKeyStore
stageLogStore stagelogstore.Store
applicationLiveStateStore applicationlivestatestore.Store
commandStore commandstore.Store
Expand Down Expand Up @@ -81,6 +82,7 @@ func NewWebAPI(
deploymentStore: datastore.NewDeploymentStore(ds),
pipedStore: datastore.NewPipedStore(ds),
projectStore: datastore.NewProjectStore(ds),
apiKeyStore: datastore.NewAPIKeyStore(ds),
stageLogStore: sls,
applicationLiveStateStore: alss,
commandStore: cmds,
Expand Down Expand Up @@ -1204,13 +1206,106 @@ L:
}

func (a *WebAPI) GenerateAPIKey(ctx context.Context, req *webservice.GenerateAPIKeyRequest) (*webservice.GenerateAPIKeyResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
claims, err := rpcauth.ExtractClaims(ctx)
if err != nil {
a.logger.Error("failed to authenticate the current user", zap.Error(err))
return nil, err
}

id := uuid.New().String()
key, hash, err := model.GenerateAPIKey(id)
if err != nil {
a.logger.Error("failed to generate API key", zap.Error(err))
return nil, status.Error(codes.Internal, "Failed to generate API key")
}

apiKey := model.APIKey{
Id: id,
Name: req.Name,
KeyHash: hash,
ProjectId: claims.Role.ProjectId,
Role: req.Role,
Creator: claims.Subject,
}

err = a.apiKeyStore.AddAPIKey(ctx, &apiKey)
if errors.Is(err, datastore.ErrAlreadyExists) {
return nil, status.Error(codes.AlreadyExists, "The API key already exists")
}
if err != nil {
a.logger.Error("failed to create API key", zap.Error(err))
return nil, status.Error(codes.Internal, "Failed to create API key")
}

return &webservice.GenerateAPIKeyResponse{
Key: key,
}, nil
}

func (a *WebAPI) DisableAPIKey(ctx context.Context, req *webservice.DisableAPIKeyRequest) (*webservice.DisableAPIKeyResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
claims, err := rpcauth.ExtractClaims(ctx)
if err != nil {
a.logger.Error("failed to authenticate the current user", zap.Error(err))
return nil, err
}

if err := a.apiKeyStore.DisableAPIKey(ctx, req.Id, claims.Role.ProjectId); err != nil {
switch err {
case datastore.ErrNotFound:
return nil, status.Error(codes.InvalidArgument, "The API key is not found")
case datastore.ErrInvalidArgument:
return nil, status.Error(codes.InvalidArgument, "Invalid value for update")
default:
a.logger.Error("failed to disable the API key",
zap.String("apikey-id", req.Id),
zap.Error(err),
)
return nil, status.Error(codes.Internal, "Failed to disable the API key")
}
}

return &webservice.DisableAPIKeyResponse{}, nil
}

func (a *WebAPI) ListAPIKeys(ctx context.Context, req *webservice.ListAPIKeysRequest) (*webservice.ListAPIKeysResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
claims, err := rpcauth.ExtractClaims(ctx)
if err != nil {
a.logger.Error("failed to authenticate the current user", zap.Error(err))
return nil, err
}

opts := datastore.ListOptions{
Filters: []datastore.ListFilter{
{
Field: "ProjectId",
Operator: "==",
Value: claims.Role.ProjectId,
},
},
}

if req.Options != nil {
if req.Options.Enabled != nil {
opts.Filters = append(opts.Filters, datastore.ListFilter{
Field: "Disabled",
Operator: "==",
Value: !req.Options.Enabled.GetValue(),
})
}
}

apiKeys, err := a.apiKeyStore.ListAPIKeys(ctx, opts)
if err != nil {
a.logger.Error("failed to list API keys", zap.Error(err))
return nil, status.Error(codes.Internal, "Failed to list API keys")
}

// Redact all sensitive data inside API key before sending to the client.
for i := range apiKeys {
apiKeys[i].RedactSensitiveData()
}

return &webservice.ListAPIKeysResponse{
Keys: apiKeys,
}, nil
}
8 changes: 8 additions & 0 deletions pkg/app/api/service/webservice/service.pb.auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ func (a *authorizer) Authorize(method string, r model.Role) bool {
return isAdmin(r)
case "/pipe.api.service.webservice.WebService/UpdateProjectRBACConfig":
return isAdmin(r)
case "/pipe.api.service.webservice.WebService/GenerateAPIKey":
return isAdmin(r)
case "/pipe.api.service.webservice.WebService/DisableAPIKey":
return isAdmin(r)
case "/pipe.api.service.webservice.WebService/ListAPIKey":
return isAdmin(r)

case "/pipe.api.service.webservice.WebService/SyncApplication":
return isAdmin(r) || isEditor(r)
case "/pipe.api.service.webservice.WebService/CancelDeployment":
Expand All @@ -78,6 +85,7 @@ func (a *authorizer) Authorize(method string, r model.Role) bool {
return isAdmin(r) || isEditor(r)
case "/pipe.api.service.webservice.WebService/GenerateApplicationSealedSecret":
return isAdmin(r) || isEditor(r)

case "/pipe.api.service.webservice.WebService/GetApplicationLiveState":
return isAdmin(r) || isEditor(r) || isViewer(r)
case "/pipe.api.service.webservice.WebService/GetProject":
Expand Down
2 changes: 1 addition & 1 deletion pkg/app/api/service/webservice/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ message GenerateAPIKeyRequest {
}

message GenerateAPIKeyResponse {
model.APIKey key = 1;
string key = 1;
}

message DisableAPIKeyRequest {
Expand Down
2 changes: 2 additions & 0 deletions pkg/datastore/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"apikey.go",
"applicationstore.go",
"commandstore.go",
"datastore.go",
Expand All @@ -25,6 +26,7 @@ go_test(
name = "go_default_test",
size = "small",
srcs = [
"apikey_test.go",
"applicationstore_test.go",
"commandstore_test.go",
"deploymentstore_test.go",
Expand Down
99 changes: 99 additions & 0 deletions pkg/datastore/apikey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2020 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package datastore

import (
"context"
"fmt"
"time"

"github.com/pipe-cd/pipe/pkg/model"
)

const apiKeyModelKind = "APIKey"

var (
apiKeyFactory = func() interface{} {
return &model.APIKey{}
}
)

type APIKeyStore interface {
AddAPIKey(ctx context.Context, k *model.APIKey) error
DisableAPIKey(ctx context.Context, id, projectID string) error
ListAPIKeys(ctx context.Context, opts ListOptions) ([]*model.APIKey, error)
}

type apiKeyStore struct {
backend
nowFunc func() time.Time
}

func NewAPIKeyStore(ds DataStore) APIKeyStore {
return &apiKeyStore{
backend: backend{
ds: ds,
},
nowFunc: time.Now,
}
}

func (s *apiKeyStore) AddAPIKey(ctx context.Context, k *model.APIKey) error {
now := s.nowFunc().Unix()
if k.CreatedAt == 0 {
k.CreatedAt = now
}
if k.UpdatedAt == 0 {
k.UpdatedAt = now
}
if err := k.Validate(); err != nil {
return err
}
return s.ds.Create(ctx, apiKeyModelKind, k.Id, k)
}

func (s *apiKeyStore) ListAPIKeys(ctx context.Context, opts ListOptions) ([]*model.APIKey, error) {
it, err := s.ds.Find(ctx, apiKeyModelKind, opts)
if err != nil {
return nil, err
}
ks := make([]*model.APIKey, 0)
for {
var k model.APIKey
err := it.Next(&k)
if err == ErrIteratorDone {
break
}
if err != nil {
return nil, err
}
ks = append(ks, &k)
}
return ks, nil
}

func (s *apiKeyStore) DisableAPIKey(ctx context.Context, id, projectID string) error {
now := s.nowFunc().Unix()
return s.ds.Update(ctx, apiKeyModelKind, id, apiKeyFactory, func(e interface{}) error {
k := e.(*model.APIKey)
if k.ProjectId != projectID {
return fmt.Errorf("invalid project id, expected %s, got %s", k.ProjectId, projectID)
}

k.Disabled = true
k.UpdatedAt = now
return k.Validate()
})
}
Loading