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
9 changes: 9 additions & 0 deletions .configs/sqlc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ sql:
package: "grpcdeploymentsql"
out: "../internal/grpc/grpcdeployment/grpcdeploymentsql"

- <<: *default_domain
name: "Service accounts SQL for gRPC"
queries: "../internal/grpc/grpcserviceaccount/queries"
gen:
go:
<<: *default_go
package: "grpcserviceaccountsql"
out: "../internal/grpc/grpcserviceaccount/grpcserviceaccountsql"

- <<: *default_domain
name: "Usersync SQL"
queries: "../internal/usersync/queries"
Expand Down
2 changes: 2 additions & 0 deletions internal/grpc/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/nais/api/internal/grpc/grpcdatabase"
"github.com/nais/api/internal/grpc/grpcdeployment"
"github.com/nais/api/internal/grpc/grpcreconciler"
"github.com/nais/api/internal/grpc/grpcserviceaccount"
"github.com/nais/api/internal/grpc/grpcteam"
"github.com/nais/api/internal/grpc/grpcuser"
"github.com/nais/api/internal/kubernetes/watchers"
Expand Down Expand Up @@ -37,6 +38,7 @@ func Run(ctx context.Context, listenAddress string, pool *pgxpool.Pool, sqlDatab
protoapi.RegisterReconcilersServer(s, grpcreconciler.NewServer(pool))
protoapi.RegisterDeploymentsServer(s, grpcdeployment.NewServer(pool))
protoapi.RegisterDatabasesServer(s, grpcdatabase.NewServer(sqlDatabaseWatcher, postgresBranchWatcher))
protoapi.RegisterServiceAccountsServer(s, grpcserviceaccount.NewServer(pool))

g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return s.Serve(lis) })
Expand Down
30 changes: 30 additions & 0 deletions internal/grpc/grpcserviceaccount/grpcserviceaccountsql/db.go

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

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

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

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

41 changes: 41 additions & 0 deletions internal/grpc/grpcserviceaccount/queries/service_accounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- name: Create :one
INSERT INTO
service_accounts (name, description, team_slug)
VALUES
(@name, @description, @team_slug)
ON CONFLICT (name, team_slug) DO UPDATE
SET
name = EXCLUDED.name
RETURNING
id,
(xmax = 0) AS created
;

-- name: AssignRole :exec
INSERT INTO
service_account_roles (service_account_id, role_name)
VALUES
(@service_account_id, @role_name)
ON CONFLICT DO NOTHING
;

-- name: CreateToken :one
INSERT INTO
service_account_tokens (
expires_at,
name,
description,
token,
service_account_id
)
VALUES
(
@expires_at,
@name,
@description,
@token,
@service_account_id
)
RETURNING
id
;
102 changes: 102 additions & 0 deletions internal/grpc/grpcserviceaccount/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package grpcserviceaccount

import (
"context"

"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/nais/api/internal/grpc/grpcserviceaccount/grpcserviceaccountsql"
"github.com/nais/api/internal/serviceaccount"
"github.com/nais/api/internal/slug"
"github.com/nais/api/pkg/apiclient/protoapi"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type Server struct {
querier grpcserviceaccountsql.Querier
protoapi.UnimplementedServiceAccountsServer
}

func NewServer(pool *pgxpool.Pool) *Server {
return &Server{
querier: grpcserviceaccountsql.New(pool),
}
}

func (s *Server) Create(ctx context.Context, req *protoapi.CreateServiceAccountRequest) (*protoapi.CreateServiceAccountResponse, error) {
teamSlug := slug.Slug(req.GetTeamSlug())
if teamSlug == "" {
return nil, status.Errorf(codes.InvalidArgument, "team_slug is required")
}
if req.GetName() == "" {
return nil, status.Errorf(codes.InvalidArgument, "name is required")
}

team := teamSlug
row, err := s.querier.Create(ctx, grpcserviceaccountsql.CreateParams{
Name: req.GetName(),
Description: req.GetDescription(),
TeamSlug: &team,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create service account: %s", err)
}

if role := req.GetRoleName(); role != "" {
err := s.querier.AssignRole(ctx, grpcserviceaccountsql.AssignRoleParams{
ServiceAccountID: row.ID,
RoleName: role,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to assign role %q: %s", role, err)
}
}

return protoapi.CreateServiceAccountResponse_builder{
Id: row.ID.String(),
Created: row.Created,
}.Build(), nil
}

func (s *Server) CreateToken(ctx context.Context, req *protoapi.CreateServiceAccountTokenRequest) (*protoapi.CreateServiceAccountTokenResponse, error) {
serviceAccountID, err := uuid.Parse(req.GetServiceAccountId())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "service_account_id must be a valid UUID: %s", err)
}
if req.GetName() == "" {
return nil, status.Errorf(codes.InvalidArgument, "name is required")
}

secret, err := serviceaccount.GenerateToken()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate token")
}
hashed, err := serviceaccount.HashToken(secret)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to hash token")
}

// expires_at is day-granular on the server side.
expiresAt := pgtype.Date{}
if req.HasExpiresAt() && req.GetExpiresAt().IsValid() {
expiresAt = pgtype.Date{Time: req.GetExpiresAt().AsTime(), Valid: true}
}

id, err := s.querier.CreateToken(ctx, grpcserviceaccountsql.CreateTokenParams{
ExpiresAt: expiresAt,
Name: req.GetName(),
Description: req.GetDescription(),
Token: hashed,
ServiceAccountID: serviceAccountID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create token: %s", err)
}

return protoapi.CreateServiceAccountTokenResponse_builder{
Id: id.String(),
Secret: secret,
}.Build(), nil
}
1 change: 1 addition & 0 deletions internal/grpc/grpcteam/grpcteamsql/querier.go

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

36 changes: 36 additions & 0 deletions internal/grpc/grpcteam/grpcteamsql/teams.sql.go

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

Loading
Loading