Compare commits

...

4 Commits

Author SHA1 Message Date
Jeff McCune
8ce88bf491 (#175) Fix goreleaser
Buf was being automatically updated in the pipeline.
2024-05-16 14:00:37 -07:00
Jeff McCune
b05571a595 (#175) Go tidy and update package.json
For goreleaser
2024-05-16 13:41:47 -07:00
Jeff McCune
4edfc71d68 (#175) Log the grpc procedure at info level
This patch logs the service and rpc method of every request at Info
level.  The error code and message is also logged.  This gives a good
indication of what rpc methods are being called and by whom.
2024-05-16 11:43:20 -07:00
Jeff McCune
3049694a0a (#175) holos register user
This patch adds a `holos register user` command.  Given an authenticated
id token and no other record of the user in the database, the cli tool
use the API to:

 1. User is registered in `holos server`
 2. User is linked to one Holos Organization.
 3. Holos Organization has the `bare` platform.
 4. Holos Organization has the `reference` platform.
 5. Ensure `~/.holos/client-context.json` contains the user id and an
    org id.

The `holos.ClientContext` struct is intended as a light weight way to
save and load the current organization id to the file system for further
API calls.

The assumption is most users will have only one single org.  We can add
a more complicated config context system like kubectl uses if and when
we need it.
2024-05-16 10:51:40 -07:00
21 changed files with 1931 additions and 1075 deletions

View File

@@ -54,6 +54,9 @@ jobs:
- name: List keys
run: gpg -K
- name: Git diff
run: git diff
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:

4
go.mod
View File

@@ -6,6 +6,7 @@ require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1
connectrpc.com/connect v1.16.0
connectrpc.com/grpcreflect v1.2.0
connectrpc.com/otelconnect v0.7.0
connectrpc.com/validate v0.1.0
cuelang.org/go v0.8.0
entgo.io/ent v0.13.1
@@ -30,6 +31,7 @@ require (
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.24.0
golang.org/x/tools v0.20.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa
google.golang.org/protobuf v1.33.1-0.20240408130810-98873a205002
honnef.co/go/tools v0.4.7
k8s.io/api v0.29.2
@@ -44,7 +46,6 @@ require (
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
connectrpc.com/otelconnect v0.7.0 // indirect
cuelabs.dev/go/oci/ociregistry v0.0.0-20240314152124-224736b49f2e // indirect
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
@@ -248,7 +249,6 @@ require (
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa // indirect
google.golang.org/grpc v1.62.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

View File

@@ -5,9 +5,11 @@ import (
"fmt"
"log/slog"
"connectrpc.com/connect"
cue "cuelang.org/go/cue/errors"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)
// MakeMain makes a main function for the cli or tests.
@@ -25,7 +27,8 @@ func MakeMain(options ...holos.Option) func() int {
// HandleError is the top level error handler that unwraps and logs errors.
func HandleError(ctx context.Context, err error, hc *holos.Config) (exitCode int) {
log := hc.NewTopLevelLogger()
// Connect errors have codes, log them.
log := hc.NewTopLevelLogger().With("code", connect.CodeOf(err))
var cueErr cue.Error
var errAt *errors.ErrorAt
const msg = "could not execute"
@@ -39,5 +42,24 @@ func HandleError(ctx context.Context, err error, hc *holos.Config) (exitCode int
msg := cue.Details(cueErr, nil)
_, _ = fmt.Fprint(hc.Stderr(), msg)
}
// connect errors have details and codes.
// Refer to https://connectrpc.com/docs/go/errors
if connectErr := new(connect.Error); errors.As(err, &connectErr) {
for _, detail := range connectErr.Details() {
msg, valueErr := detail.Value()
if valueErr != nil {
log.WarnContext(ctx, "could not decode error detail", "err", err, "type", detail.Type(), "note", "this usually means we don't have the schema for the protobuf message type")
continue
}
if info, ok := msg.(*errdetails.ErrorInfo); ok {
logDetail := log.With("reason", info.GetReason(), "domain", info.GetDomain())
for k, v := range info.GetMetadata() {
logDetail = logDetail.With(k, v)
}
logDetail.ErrorContext(ctx, info.String())
}
}
}
return 1
}

View File

@@ -0,0 +1,34 @@
package register
import (
"github.com/holos-run/holos/internal/cli/command"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/register"
"github.com/spf13/cobra"
)
// New returns a new register command.
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("register")
cmd.Short = "register with holos server"
cmd.Args = cobra.NoArgs
config := register.NewConfig(cfg)
cmd.PersistentFlags().AddGoFlagSet(config.ClientFlagSet())
cmd.PersistentFlags().AddGoFlagSet(config.TokenFlagSet())
cmd.AddCommand(NewUser(config))
return cmd
}
// NewUser returns a command to register a user with holos server.
func NewUser(cfg *register.Config) *cobra.Command {
cmd := command.New("user")
cmd.Short = "user registration workflow"
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
return register.User(ctx, cfg)
}
return cmd
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/holos-run/holos/internal/cli/login"
"github.com/holos-run/holos/internal/cli/logout"
"github.com/holos-run/holos/internal/cli/preflight"
"github.com/holos-run/holos/internal/cli/register"
"github.com/holos-run/holos/internal/cli/render"
"github.com/holos-run/holos/internal/cli/rpc"
"github.com/holos-run/holos/internal/cli/token"
@@ -68,6 +69,7 @@ func New(cfg *holos.Config) *cobra.Command {
rootCmd.AddCommand(token.New(cfg))
rootCmd.AddCommand(rpc.New(cfg))
rootCmd.AddCommand(generate.New(cfg))
rootCmd.AddCommand(register.New(cfg))
// Maybe not needed?
rootCmd.AddCommand(txtar.New(cfg))

View File

@@ -45,7 +45,7 @@ func NewPlatformModel(cfg *Config) *cobra.Command {
cmd := command.New("platform-model")
cmd.Short = "get the platform model"
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
ctx := cmd.Root().Context()
log := logger.FromContext(ctx)
// client := platformconnect.NewPlatformServiceClient(token.NewClient(cfg.token), cfg.client.Server())
client := platformconnect.NewPlatformServiceClient(token.NewClient(cfg.token), cfg.client.Server())

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@
"@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "^17.3.4",
"@angular/compiler-cli": "^17.3.0",
"@bufbuild/buf": "^1.31.0",
"@bufbuild/buf": "^1.32.0",
"@bufbuild/protoc-gen-es": "^1.9.0",
"@connectrpc/protoc-gen-connect-es": "^1.4.0",
"@connectrpc/protoc-gen-connect-query": "^1.4.0",

View File

@@ -3,7 +3,7 @@
/* eslint-disable */
// @ts-nocheck
import { CreateUserRequest, CreateUserResponse, GetUserRequest, GetUserResponse } from "./user_service_pb.js";
import { CreateUserRequest, CreateUserResponse, GetUserRequest, GetUserResponse, RegisterUserRequest, RegisterUserResponse } from "./user_service_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
/**
@@ -36,6 +36,17 @@ export const UserService = {
O: GetUserResponse,
kind: MethodKind.Unary,
},
/**
* Register an user and initialize an organization, bare platform, and reference platform.
*
* @generated from rpc holos.user.v1alpha1.UserService.RegisterUser
*/
registerUser: {
name: "RegisterUser",
I: RegisterUserRequest,
O: RegisterUserResponse,
kind: MethodKind.Unary,
},
}
} as const;

View File

@@ -7,6 +7,7 @@ import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialM
import { FieldMask, Message, proto3 } from "@bufbuild/protobuf";
import { User } from "./user_pb.js";
import { UserRef } from "../../object/v1alpha1/object_pb.js";
import { Organization } from "../../organization/v1alpha1/organization_pb.js";
/**
* Create a User from the oidc id token claims or the provided user. Each one
@@ -172,3 +173,118 @@ export class GetUserResponse extends Message<GetUserResponse> {
}
}
/**
* Register a User from the oidc id token claims or the provided user. Each one
* of subject, email, and user id must be globally unique.
*
* @generated from message holos.user.v1alpha1.RegisterUserRequest
*/
export class RegisterUserRequest extends Message<RegisterUserRequest> {
/**
* User resource to create. If absent, the server populates User fields with
* the oidc id token claims of the authenticated request.
* NOTE: The server may ignore this request field and register the user solely
* from authenticated identity claims.
*
* @generated from field: optional holos.user.v1alpha1.User user = 1;
*/
user?: User;
/**
* Mask of the user fields to include in the response.
*
* @generated from field: optional google.protobuf.FieldMask user_mask = 2;
*/
userMask?: FieldMask;
/**
* Organization resource to create. If absent, the server generates an
* organization based on the user fields.
* NOTE: The server may ignore this request field and register the
* organization solely from authenticated identity claims.
*
* @generated from field: optional holos.organization.v1alpha1.Organization organization = 3;
*/
organization?: Organization;
/**
* Mask of the organization fields to include in the response.
*
* @generated from field: optional google.protobuf.FieldMask organization_mask = 4;
*/
organizationMask?: FieldMask;
constructor(data?: PartialMessage<RegisterUserRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.user.v1alpha1.RegisterUserRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "user", kind: "message", T: User, opt: true },
{ no: 2, name: "user_mask", kind: "message", T: FieldMask, opt: true },
{ no: 3, name: "organization", kind: "message", T: Organization, opt: true },
{ no: 4, name: "organization_mask", kind: "message", T: FieldMask, opt: true },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RegisterUserRequest {
return new RegisterUserRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RegisterUserRequest {
return new RegisterUserRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RegisterUserRequest {
return new RegisterUserRequest().fromJsonString(jsonString, options);
}
static equals(a: RegisterUserRequest | PlainMessage<RegisterUserRequest> | undefined, b: RegisterUserRequest | PlainMessage<RegisterUserRequest> | undefined): boolean {
return proto3.util.equals(RegisterUserRequest, a, b);
}
}
/**
* @generated from message holos.user.v1alpha1.RegisterUserResponse
*/
export class RegisterUserResponse extends Message<RegisterUserResponse> {
/**
* @generated from field: holos.user.v1alpha1.User user = 1;
*/
user?: User;
/**
* @generated from field: holos.organization.v1alpha1.Organization organization = 2;
*/
organization?: Organization;
constructor(data?: PartialMessage<RegisterUserResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.user.v1alpha1.RegisterUserResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "user", kind: "message", T: User },
{ no: 2, name: "organization", kind: "message", T: Organization },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RegisterUserResponse {
return new RegisterUserResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RegisterUserResponse {
return new RegisterUserResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RegisterUserResponse {
return new RegisterUserResponse().fromJsonString(jsonString, options);
}
static equals(a: RegisterUserResponse | PlainMessage<RegisterUserResponse> | undefined, b: RegisterUserResponse | PlainMessage<RegisterUserResponse> | undefined): boolean {
return proto3.util.equals(RegisterUserResponse, a, b);
}
}

View File

@@ -0,0 +1,70 @@
package holos
import (
"context"
"encoding/json"
"log/slog"
"os"
"path/filepath"
"github.com/holos-run/holos/internal/logger"
"k8s.io/client-go/util/homedir"
)
// ClientContext represents the context the holos api is working in. Used to
// store and recall values from the filesystem.
type ClientContext struct {
// OrgID is the organization id of the current context.
OrgID string `json:"org_id"`
// UserID is the user id of the current context.
UserID string `json:"user_id"`
}
func (cc *ClientContext) Save(ctx context.Context) error {
log := logger.FromContext(ctx)
config := cc.configFile()
data, err := json.MarshalIndent(cc, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(config, data, 0644); err != nil {
return err
}
log.DebugContext(ctx, "saved", "path", config, "bytes", len(data))
return nil
}
func (cc *ClientContext) Load(ctx context.Context) error {
log := logger.FromContext(ctx)
config := cc.configFile()
data, err := os.ReadFile(config)
if err != nil {
return err
}
if err := json.Unmarshal(data, cc); err != nil {
return err
}
log.DebugContext(ctx, "loaded", "path", config, "bytes", len(data))
return nil
}
// Exists returns true if the client context file exists.
func (cc *ClientContext) Exists() bool {
_, err := os.Stat(cc.configFile())
if os.IsNotExist(err) {
return false
}
return err == nil
}
func (cc *ClientContext) configFile() string {
config := "client-context.json"
if home := homedir.HomeDir(); home != "" {
dir := filepath.Join(home, ".holos")
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
slog.Warn("could not mkdir", "path", dir, "err", err)
}
config = filepath.Join(home, ".holos", config)
}
return config
}

View File

@@ -0,0 +1,134 @@
package register
import (
"context"
"flag"
"connectrpc.com/connect"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/server/middleware/logger"
"github.com/holos-run/holos/internal/token"
org "github.com/holos-run/holos/service/gen/holos/organization/v1alpha1"
"github.com/holos-run/holos/service/gen/holos/organization/v1alpha1/organizationconnect"
user "github.com/holos-run/holos/service/gen/holos/user/v1alpha1"
"github.com/holos-run/holos/service/gen/holos/user/v1alpha1/userconnect"
)
type Config struct {
holos *holos.Config
client *holos.ClientConfig
token *token.Config
}
func (c *Config) ClientFlagSet() *flag.FlagSet {
if c == nil {
return nil
}
return c.client.FlagSet()
}
func (c *Config) TokenFlagSet() *flag.FlagSet {
if c == nil {
return nil
}
return c.token.FlagSet()
}
func NewConfig(cfg *holos.Config) *Config {
return &Config{
holos: cfg,
client: holos.NewClientConfig(),
token: token.NewConfig(),
}
}
// User registers the user with the holos server.
func User(ctx context.Context, cfg *Config) error {
log := logger.FromContext(ctx)
client := userconnect.NewUserServiceClient(token.NewClient(cfg.token), cfg.client.Server())
var err error
var u *user.User
var o *org.Organization
cc := &holos.ClientContext{}
u, err = getUser(ctx, client)
if err != nil {
if connect.CodeOf(err) != connect.CodeNotFound {
return errors.Wrap(err)
}
if u, o, err = registerUser(ctx, client); err != nil {
return errors.Wrap(err)
}
// Save the registration context
cc.OrgID = o.GetOrgId()
cc.UserID = u.GetId()
if err := cc.Save(ctx); err != nil {
return errors.Wrap(err)
}
log.InfoContext(ctx, "created user", "email", u.GetEmail(), "id", u.GetId())
}
if cc.Exists() {
if err := cc.Load(ctx); err != nil {
return errors.Wrap(err)
}
}
// Ensure the current user id gets saved.
cc.UserID = u.GetId()
// Ensure an org ID gets saved.
if cc.OrgID == "" {
org, err := getOrg(ctx, cfg)
if err != nil {
return errors.Wrap(err)
}
cc.OrgID = org.GetOrgId()
}
// One last save, we know we have the user id and org id at this point.
if err := cc.Save(ctx); err != nil {
return errors.Wrap(err)
}
log.InfoContext(ctx, "user", "email", u.GetEmail(), "user_id", cc.UserID, "org_id", cc.OrgID)
return nil
}
func getUser(ctx context.Context, client userconnect.UserServiceClient) (*user.User, error) {
req := connect.NewRequest(&user.GetUserRequest{})
resp, err := client.GetUser(ctx, req)
if err != nil {
return nil, errors.Wrap(err)
}
return resp.Msg.GetUser(), nil
}
// getOrg returns the first organization returned from the ListOrganizations rpc
// method.
func getOrg(ctx context.Context, cfg *Config) (*org.Organization, error) {
client := organizationconnect.NewOrganizationServiceClient(token.NewClient(cfg.token), cfg.client.Server())
req := connect.NewRequest(&org.ListOrganizationsRequest{})
resp, err := client.ListOrganizations(ctx, req)
if err != nil {
return nil, errors.Wrap(err)
}
orgs := resp.Msg.GetOrganizations()
if len(orgs) == 0 {
return nil, nil
} else {
return orgs[0], nil
}
}
func registerUser(ctx context.Context, client userconnect.UserServiceClient) (*user.User, *org.Organization, error) {
req := connect.NewRequest(&user.RegisterUserRequest{})
resp, err := client.RegisterUser(ctx, req)
if err != nil {
return nil, nil, errors.Wrap(err)
}
return resp.Msg.GetUser(), resp.Msg.GetOrganization(), nil
}

View File

@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"strings"
"connectrpc.com/connect"
@@ -20,7 +21,7 @@ import (
fieldmask_utils "github.com/mennanov/fieldmask-utils"
)
const AdminEmail = "jeff@openinfrastructure.co"
var adminEmails = []string{"jeff@openinfrastructure.co", "jeff@ois.run"}
// NewSystemHandler returns a new SystemService implementation.
func NewSystemHandler(db *ent.Client) *SystemHandler {
@@ -37,8 +38,8 @@ func (h *SystemHandler) checkAdmin(ctx context.Context) error {
if err != nil {
return connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
if authnID.Email() != AdminEmail {
err := fmt.Errorf("not an admin:\n\thave (%+v)\n\twant (%+v)", authnID.Email(), AdminEmail)
if !slices.Contains(adminEmails, authnID.Email()) {
err := fmt.Errorf("not an admin:\n\thave (%+v)\n\twant (%+v)", authnID.Email(), strings.Join(adminEmails, ","))
return connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
return nil

View File

@@ -1,18 +1,26 @@
package handler
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"connectrpc.com/connect"
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent"
"github.com/holos-run/holos/internal/ent/user"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/server/middleware/authn"
"github.com/holos-run/holos/internal/strings"
object "github.com/holos-run/holos/service/gen/holos/object/v1alpha1"
org "github.com/holos-run/holos/service/gen/holos/organization/v1alpha1"
storage "github.com/holos-run/holos/service/gen/holos/storage/v1alpha1"
holos "github.com/holos-run/holos/service/gen/holos/user/v1alpha1"
fieldmask_utils "github.com/mennanov/fieldmask-utils"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -50,8 +58,12 @@ func (h *UserHandler) GetUser(ctx context.Context, req *connect.Request[holos.Ge
}
func (h *UserHandler) CreateUser(ctx context.Context, req *connect.Request[holos.CreateUserRequest]) (*connect.Response[holos.CreateUserResponse], error) {
_, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
var createdUser *ent.User
var err error
if rpcUser := req.Msg.GetUser(); rpcUser != nil {
createdUser, err = h.createUser(ctx, h.db, rpcUser)
} else {
@@ -67,6 +79,145 @@ func (h *UserHandler) CreateUser(ctx context.Context, req *connect.Request[holos
return res, nil
}
func (h *UserHandler) RegisterUser(ctx context.Context, req *connect.Request[holos.RegisterUserRequest]) (*connect.Response[holos.RegisterUserResponse], error) {
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
var dbUser *ent.User
var dbOrg *ent.Organization
var rpcUser holos.User
var rpcOrg org.Organization
userMask, err := fieldmask_utils.MaskFromProtoFieldMask(req.Msg.GetUserMask(), strings.PascalCase)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err))
}
orgMask, err := fieldmask_utils.MaskFromProtoFieldMask(req.Msg.GetOrganizationMask(), strings.PascalCase)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err))
}
// Server assigns IDs.
userID, err := uuid.NewV7()
if err != nil {
return nil, errors.Wrap(connect.NewError(connect.CodeInternal, err))
}
orgID, err := uuid.NewV7()
if err != nil {
return nil, errors.Wrap(connect.NewError(connect.CodeInternal, err))
}
bareID, err := uuid.NewV7()
if err != nil {
return nil, errors.Wrap(connect.NewError(connect.CodeInternal, err))
}
refID, err := uuid.NewV7()
if err != nil {
return nil, errors.Wrap(connect.NewError(connect.CodeInternal, err))
}
// Perform registration in a single transaction.
if err := WithTx(ctx, h.db, func(tx *ent.Tx) (err error) {
// Create the user
dbUser, err = tx.User.Create().
SetID(userID).
SetEmail(authnID.Email()).
SetIss(authnID.Issuer()).
SetSub(authnID.Subject()).
SetName(authnID.Name()).
Save(ctx)
if err != nil {
if ent.IsConstraintError(err) {
rpcErr := connect.NewError(connect.CodeAlreadyExists, errors.New("user already registered"))
errInfo := &errdetails.ErrorInfo{
Reason: "USER_EXISTS",
Domain: "user.holos.run",
Metadata: map[string]string{"email": authnID.Email()},
}
if detail, detailErr := connect.NewErrorDetail(errInfo); detailErr != nil {
logger.FromContext(ctx).ErrorContext(ctx, detailErr.Error(), "err", detailErr)
} else {
rpcErr.AddDetail(detail)
}
return errors.Wrap(rpcErr)
}
return errors.Wrap(err)
}
// Create the org
dbOrg, err = tx.Organization.Create().
SetID(orgID).
SetName(cleanAndAppendRandom(authnID.Name())).
SetDisplayName(authnID.GivenName() + "'s Org").
SetCreatorID(userID).
SetEditorID(userID).
AddUserIDs(userID).
Save(ctx)
if err != nil {
return errors.Wrap(err)
}
// Create the platforms.
decoder := json.NewDecoder(bytes.NewReader([]byte(BareForm)))
decoder.DisallowUnknownFields()
var form storage.Form
if err := decoder.Decode(&form); err != nil {
return errors.Wrap(err)
}
decoder = json.NewDecoder(bytes.NewReader([]byte(Model)))
decoder.DisallowUnknownFields()
var model storage.Model
if err := decoder.Decode(&model); err != nil {
return errors.Wrap(err)
}
// Add the platforms.
err = tx.Platform.Create().
SetID(bareID).
SetName("bare").
SetDisplayName("Bare Platform").
SetForm(&form).
SetModel(&model).
SetCreatorID(userID).
SetEditorID(userID).
SetOrgID(orgID).
Exec(ctx)
if err != nil {
return errors.Wrap(err)
}
err = tx.Platform.Create().
SetID(refID).
SetName("reference").
SetDisplayName("Holos Reference Platform").
SetForm(&form).
SetModel(&model).
SetCreatorID(userID).
SetEditorID(userID).
SetOrgID(orgID).
Exec(ctx)
if err != nil {
return errors.Wrap(err)
}
return nil
}); err != nil {
return nil, errors.Wrap(connect.NewError(connect.CodeFailedPrecondition, err))
}
if err = fieldmask_utils.StructToStruct(userMask, UserToRPC(dbUser), &rpcUser); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err))
}
if err = fieldmask_utils.StructToStruct(orgMask, OrganizationToRPC(dbOrg), &rpcOrg); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err))
}
msg := holos.RegisterUserResponse{
User: &rpcUser,
Organization: &rpcOrg,
}
return connect.NewResponse(&msg), nil
}
// UserToRPC returns an *holos.User adapted from *ent.User u.
func UserToRPC(entity *ent.User) *holos.User {
uid := entity.ID.String()

View File

@@ -0,0 +1,33 @@
package interceptor
import (
"context"
"time"
"connectrpc.com/connect"
"github.com/holos-run/holos/internal/server/middleware/logger"
)
func NewLogger() connect.UnaryInterceptorFunc {
interceptor := func(next connect.UnaryFunc) connect.UnaryFunc {
return connect.UnaryFunc(func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
start := time.Now()
rpcLogger := logger.FromContext(ctx).With("procedure", req.Spec().Procedure)
ctx = logger.NewContext(ctx, rpcLogger)
resp, err := next(ctx, req)
go emitLog(ctx, start, err)
return resp, err
})
}
return connect.UnaryInterceptorFunc(interceptor)
}
func emitLog(ctx context.Context, start time.Time, err error) {
log := logger.FromContext(ctx)
if err == nil {
log = log.With("ok", true)
} else {
log = log.With("ok", false, "code", connect.CodeOf(err), "err", err)
}
log.InfoContext(ctx, "response", "duration", time.Since(start))
}

View File

@@ -9,12 +9,14 @@ import (
"connectrpc.com/connect"
"connectrpc.com/grpcreflect"
"connectrpc.com/otelconnect"
"connectrpc.com/validate"
"github.com/holos-run/holos/internal/ent"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/frontend"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/server/handler"
"github.com/holos-run/holos/internal/server/interceptor"
"github.com/holos-run/holos/internal/server/middleware/authn"
"github.com/holos-run/holos/internal/server/middleware/logger"
"github.com/holos-run/holos/service/gen/holos/organization/v1alpha1/organizationconnect"
@@ -113,7 +115,12 @@ func (s *Server) registerConnectRpc() error {
return errors.Wrap(fmt.Errorf("could not initialize proto validation interceptor: %w", err))
}
opts := connect.WithInterceptors(validator)
otel, err := otelconnect.NewInterceptor()
if err != nil {
return errors.Wrap(err)
}
opts := connect.WithInterceptors(interceptor.NewLogger(), otel, validator)
s.handle(userconnect.NewUserServiceHandler(handler.NewUserHandler(s.db), opts))
s.handle(organizationconnect.NewOrganizationServiceHandler(handler.NewOrganizationHandler(s.db), opts))

View File

@@ -8,6 +8,7 @@ package user
import (
v1alpha1 "github.com/holos-run/holos/service/gen/holos/object/v1alpha1"
v1alpha11 "github.com/holos-run/holos/service/gen/holos/organization/v1alpha1"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb"
@@ -224,6 +225,144 @@ func (x *GetUserResponse) GetUser() *User {
return nil
}
// Register a User from the oidc id token claims or the provided user. Each one
// of subject, email, and user id must be globally unique.
type RegisterUserRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// User resource to create. If absent, the server populates User fields with
// the oidc id token claims of the authenticated request.
// NOTE: The server may ignore this request field and register the user solely
// from authenticated identity claims.
User *User `protobuf:"bytes,1,opt,name=user,proto3,oneof" json:"user,omitempty"`
// Mask of the user fields to include in the response.
UserMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=user_mask,json=userMask,proto3,oneof" json:"user_mask,omitempty"`
// Organization resource to create. If absent, the server generates an
// organization based on the user fields.
// NOTE: The server may ignore this request field and register the
// organization solely from authenticated identity claims.
Organization *v1alpha11.Organization `protobuf:"bytes,3,opt,name=organization,proto3,oneof" json:"organization,omitempty"`
// Mask of the organization fields to include in the response.
OrganizationMask *fieldmaskpb.FieldMask `protobuf:"bytes,4,opt,name=organization_mask,json=organizationMask,proto3,oneof" json:"organization_mask,omitempty"`
}
func (x *RegisterUserRequest) Reset() {
*x = RegisterUserRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_holos_user_v1alpha1_user_service_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RegisterUserRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RegisterUserRequest) ProtoMessage() {}
func (x *RegisterUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_holos_user_v1alpha1_user_service_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RegisterUserRequest.ProtoReflect.Descriptor instead.
func (*RegisterUserRequest) Descriptor() ([]byte, []int) {
return file_holos_user_v1alpha1_user_service_proto_rawDescGZIP(), []int{4}
}
func (x *RegisterUserRequest) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
func (x *RegisterUserRequest) GetUserMask() *fieldmaskpb.FieldMask {
if x != nil {
return x.UserMask
}
return nil
}
func (x *RegisterUserRequest) GetOrganization() *v1alpha11.Organization {
if x != nil {
return x.Organization
}
return nil
}
func (x *RegisterUserRequest) GetOrganizationMask() *fieldmaskpb.FieldMask {
if x != nil {
return x.OrganizationMask
}
return nil
}
type RegisterUserResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
Organization *v1alpha11.Organization `protobuf:"bytes,2,opt,name=organization,proto3" json:"organization,omitempty"`
}
func (x *RegisterUserResponse) Reset() {
*x = RegisterUserResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_holos_user_v1alpha1_user_service_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RegisterUserResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RegisterUserResponse) ProtoMessage() {}
func (x *RegisterUserResponse) ProtoReflect() protoreflect.Message {
mi := &file_holos_user_v1alpha1_user_service_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RegisterUserResponse.ProtoReflect.Descriptor instead.
func (*RegisterUserResponse) Descriptor() ([]byte, []int) {
return file_holos_user_v1alpha1_user_service_proto_rawDescGZIP(), []int{5}
}
func (x *RegisterUserResponse) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
func (x *RegisterUserResponse) GetOrganization() *v1alpha11.Organization {
if x != nil {
return x.Organization
}
return nil
}
var File_holos_user_v1alpha1_user_service_proto protoreflect.FileDescriptor
var file_holos_user_v1alpha1_user_service_proto_rawDesc = []byte{
@@ -232,7 +371,10 @@ var file_holos_user_v1alpha1_user_service_proto_rawDesc = []byte{
0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e,
0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x1a, 0x1e, 0x68,
0x6f, 0x6c, 0x6f, 0x73, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
0x61, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67,
0x61, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2e, 0x68,
0x6f, 0x6c, 0x6f, 0x73, 0x2f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6f, 0x72, 0x67, 0x61, 0x6e,
0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66,
0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
0x22, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x76, 0x31,
@@ -260,24 +402,62 @@ var file_holos_user_v1alpha1_user_service_proto_rawDesc = []byte{
0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x19, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76,
0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73,
0x65, 0x72, 0x32, 0xc6, 0x01, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x12, 0x5f, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72,
0x12, 0x26, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65,
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73,
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43,
0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x23,
0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x61, 0x6c,
0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72,
0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65,
0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x41, 0x5a, 0x3f, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2d,
0x72, 0x75, 0x6e, 0x2f, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2f, 0x75, 0x73, 0x65, 0x72,
0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x65, 0x72, 0x22, 0xe7, 0x02, 0x0a, 0x13, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x55,
0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x04, 0x75, 0x73,
0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73,
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x55,
0x73, 0x65, 0x72, 0x48, 0x00, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x3c,
0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x48, 0x01, 0x52,
0x08, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x61, 0x73, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x52, 0x0a, 0x0c,
0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x29, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x6f, 0x72, 0x67, 0x61, 0x6e,
0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x02, 0x52,
0x0c, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01,
0x12, 0x4c, 0x0a, 0x11, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69,
0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x48, 0x03, 0x52, 0x10, 0x6f, 0x72, 0x67, 0x61, 0x6e,
0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x73, 0x6b, 0x88, 0x01, 0x01, 0x42, 0x07,
0x0a, 0x05, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69,
0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x6f, 0x72, 0x67, 0x61, 0x6e,
0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x22, 0x94, 0x01, 0x0a,
0x14, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72,
0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04,
0x75, 0x73, 0x65, 0x72, 0x12, 0x4d, 0x0a, 0x0c, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x68, 0x6f, 0x6c,
0x6f, 0x73, 0x2e, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e,
0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x32, 0xad, 0x02, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65,
0x72, 0x12, 0x26, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76,
0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x68, 0x6f, 0x6c, 0x6f,
0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12,
0x23, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x61,
0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x75, 0x73, 0x65,
0x72, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73,
0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, 0x0c,
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, 0x28, 0x2e, 0x68,
0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
0x61, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2e, 0x75,
0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x67,
0x69, 0x73, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x00, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x68, 0x6f, 0x6c, 0x6f, 0x73, 0x2d, 0x72, 0x75, 0x6e, 0x2f, 0x68, 0x6f, 0x6c, 0x6f,
0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x68, 0x6f,
0x6c, 0x6f, 0x73, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -292,31 +472,42 @@ func file_holos_user_v1alpha1_user_service_proto_rawDescGZIP() []byte {
return file_holos_user_v1alpha1_user_service_proto_rawDescData
}
var file_holos_user_v1alpha1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_holos_user_v1alpha1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_holos_user_v1alpha1_user_service_proto_goTypes = []interface{}{
(*CreateUserRequest)(nil), // 0: holos.user.v1alpha1.CreateUserRequest
(*CreateUserResponse)(nil), // 1: holos.user.v1alpha1.CreateUserResponse
(*GetUserRequest)(nil), // 2: holos.user.v1alpha1.GetUserRequest
(*GetUserResponse)(nil), // 3: holos.user.v1alpha1.GetUserResponse
(*User)(nil), // 4: holos.user.v1alpha1.User
(*v1alpha1.UserRef)(nil), // 5: holos.object.v1alpha1.UserRef
(*fieldmaskpb.FieldMask)(nil), // 6: google.protobuf.FieldMask
(*CreateUserRequest)(nil), // 0: holos.user.v1alpha1.CreateUserRequest
(*CreateUserResponse)(nil), // 1: holos.user.v1alpha1.CreateUserResponse
(*GetUserRequest)(nil), // 2: holos.user.v1alpha1.GetUserRequest
(*GetUserResponse)(nil), // 3: holos.user.v1alpha1.GetUserResponse
(*RegisterUserRequest)(nil), // 4: holos.user.v1alpha1.RegisterUserRequest
(*RegisterUserResponse)(nil), // 5: holos.user.v1alpha1.RegisterUserResponse
(*User)(nil), // 6: holos.user.v1alpha1.User
(*v1alpha1.UserRef)(nil), // 7: holos.object.v1alpha1.UserRef
(*fieldmaskpb.FieldMask)(nil), // 8: google.protobuf.FieldMask
(*v1alpha11.Organization)(nil), // 9: holos.organization.v1alpha1.Organization
}
var file_holos_user_v1alpha1_user_service_proto_depIdxs = []int32{
4, // 0: holos.user.v1alpha1.CreateUserRequest.user:type_name -> holos.user.v1alpha1.User
4, // 1: holos.user.v1alpha1.CreateUserResponse.user:type_name -> holos.user.v1alpha1.User
5, // 2: holos.user.v1alpha1.GetUserRequest.user:type_name -> holos.object.v1alpha1.UserRef
6, // 3: holos.user.v1alpha1.GetUserRequest.field_mask:type_name -> google.protobuf.FieldMask
4, // 4: holos.user.v1alpha1.GetUserResponse.user:type_name -> holos.user.v1alpha1.User
0, // 5: holos.user.v1alpha1.UserService.CreateUser:input_type -> holos.user.v1alpha1.CreateUserRequest
2, // 6: holos.user.v1alpha1.UserService.GetUser:input_type -> holos.user.v1alpha1.GetUserRequest
1, // 7: holos.user.v1alpha1.UserService.CreateUser:output_type -> holos.user.v1alpha1.CreateUserResponse
3, // 8: holos.user.v1alpha1.UserService.GetUser:output_type -> holos.user.v1alpha1.GetUserResponse
7, // [7:9] is the sub-list for method output_type
5, // [5:7] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
6, // 0: holos.user.v1alpha1.CreateUserRequest.user:type_name -> holos.user.v1alpha1.User
6, // 1: holos.user.v1alpha1.CreateUserResponse.user:type_name -> holos.user.v1alpha1.User
7, // 2: holos.user.v1alpha1.GetUserRequest.user:type_name -> holos.object.v1alpha1.UserRef
8, // 3: holos.user.v1alpha1.GetUserRequest.field_mask:type_name -> google.protobuf.FieldMask
6, // 4: holos.user.v1alpha1.GetUserResponse.user:type_name -> holos.user.v1alpha1.User
6, // 5: holos.user.v1alpha1.RegisterUserRequest.user:type_name -> holos.user.v1alpha1.User
8, // 6: holos.user.v1alpha1.RegisterUserRequest.user_mask:type_name -> google.protobuf.FieldMask
9, // 7: holos.user.v1alpha1.RegisterUserRequest.organization:type_name -> holos.organization.v1alpha1.Organization
8, // 8: holos.user.v1alpha1.RegisterUserRequest.organization_mask:type_name -> google.protobuf.FieldMask
6, // 9: holos.user.v1alpha1.RegisterUserResponse.user:type_name -> holos.user.v1alpha1.User
9, // 10: holos.user.v1alpha1.RegisterUserResponse.organization:type_name -> holos.organization.v1alpha1.Organization
0, // 11: holos.user.v1alpha1.UserService.CreateUser:input_type -> holos.user.v1alpha1.CreateUserRequest
2, // 12: holos.user.v1alpha1.UserService.GetUser:input_type -> holos.user.v1alpha1.GetUserRequest
4, // 13: holos.user.v1alpha1.UserService.RegisterUser:input_type -> holos.user.v1alpha1.RegisterUserRequest
1, // 14: holos.user.v1alpha1.UserService.CreateUser:output_type -> holos.user.v1alpha1.CreateUserResponse
3, // 15: holos.user.v1alpha1.UserService.GetUser:output_type -> holos.user.v1alpha1.GetUserResponse
5, // 16: holos.user.v1alpha1.UserService.RegisterUser:output_type -> holos.user.v1alpha1.RegisterUserResponse
14, // [14:17] is the sub-list for method output_type
11, // [11:14] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
}
func init() { file_holos_user_v1alpha1_user_service_proto_init() }
@@ -374,16 +565,41 @@ func file_holos_user_v1alpha1_user_service_proto_init() {
return nil
}
}
file_holos_user_v1alpha1_user_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RegisterUserRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_holos_user_v1alpha1_user_service_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RegisterUserResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_holos_user_v1alpha1_user_service_proto_msgTypes[0].OneofWrappers = []interface{}{}
file_holos_user_v1alpha1_user_service_proto_msgTypes[2].OneofWrappers = []interface{}{}
file_holos_user_v1alpha1_user_service_proto_msgTypes[4].OneofWrappers = []interface{}{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_holos_user_v1alpha1_user_service_proto_rawDesc,
NumEnums: 0,
NumMessages: 4,
NumMessages: 6,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -37,13 +37,17 @@ const (
UserServiceCreateUserProcedure = "/holos.user.v1alpha1.UserService/CreateUser"
// UserServiceGetUserProcedure is the fully-qualified name of the UserService's GetUser RPC.
UserServiceGetUserProcedure = "/holos.user.v1alpha1.UserService/GetUser"
// UserServiceRegisterUserProcedure is the fully-qualified name of the UserService's RegisterUser
// RPC.
UserServiceRegisterUserProcedure = "/holos.user.v1alpha1.UserService/RegisterUser"
)
// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package.
var (
userServiceServiceDescriptor = v1alpha1.File_holos_user_v1alpha1_user_service_proto.Services().ByName("UserService")
userServiceCreateUserMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("CreateUser")
userServiceGetUserMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("GetUser")
userServiceServiceDescriptor = v1alpha1.File_holos_user_v1alpha1_user_service_proto.Services().ByName("UserService")
userServiceCreateUserMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("CreateUser")
userServiceGetUserMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("GetUser")
userServiceRegisterUserMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("RegisterUser")
)
// UserServiceClient is a client for the holos.user.v1alpha1.UserService service.
@@ -52,6 +56,8 @@ type UserServiceClient interface {
CreateUser(context.Context, *connect.Request[v1alpha1.CreateUserRequest]) (*connect.Response[v1alpha1.CreateUserResponse], error)
// Get an existing user by id, email, or subject.
GetUser(context.Context, *connect.Request[v1alpha1.GetUserRequest]) (*connect.Response[v1alpha1.GetUserResponse], error)
// Register an user and initialize an organization, bare platform, and reference platform.
RegisterUser(context.Context, *connect.Request[v1alpha1.RegisterUserRequest]) (*connect.Response[v1alpha1.RegisterUserResponse], error)
}
// NewUserServiceClient constructs a client for the holos.user.v1alpha1.UserService service. By
@@ -76,13 +82,20 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
connect.WithSchema(userServiceGetUserMethodDescriptor),
connect.WithClientOptions(opts...),
),
registerUser: connect.NewClient[v1alpha1.RegisterUserRequest, v1alpha1.RegisterUserResponse](
httpClient,
baseURL+UserServiceRegisterUserProcedure,
connect.WithSchema(userServiceRegisterUserMethodDescriptor),
connect.WithClientOptions(opts...),
),
}
}
// userServiceClient implements UserServiceClient.
type userServiceClient struct {
createUser *connect.Client[v1alpha1.CreateUserRequest, v1alpha1.CreateUserResponse]
getUser *connect.Client[v1alpha1.GetUserRequest, v1alpha1.GetUserResponse]
createUser *connect.Client[v1alpha1.CreateUserRequest, v1alpha1.CreateUserResponse]
getUser *connect.Client[v1alpha1.GetUserRequest, v1alpha1.GetUserResponse]
registerUser *connect.Client[v1alpha1.RegisterUserRequest, v1alpha1.RegisterUserResponse]
}
// CreateUser calls holos.user.v1alpha1.UserService.CreateUser.
@@ -95,12 +108,19 @@ func (c *userServiceClient) GetUser(ctx context.Context, req *connect.Request[v1
return c.getUser.CallUnary(ctx, req)
}
// RegisterUser calls holos.user.v1alpha1.UserService.RegisterUser.
func (c *userServiceClient) RegisterUser(ctx context.Context, req *connect.Request[v1alpha1.RegisterUserRequest]) (*connect.Response[v1alpha1.RegisterUserResponse], error) {
return c.registerUser.CallUnary(ctx, req)
}
// UserServiceHandler is an implementation of the holos.user.v1alpha1.UserService service.
type UserServiceHandler interface {
// Create a new user from authenticated claims or the provided User resource.
CreateUser(context.Context, *connect.Request[v1alpha1.CreateUserRequest]) (*connect.Response[v1alpha1.CreateUserResponse], error)
// Get an existing user by id, email, or subject.
GetUser(context.Context, *connect.Request[v1alpha1.GetUserRequest]) (*connect.Response[v1alpha1.GetUserResponse], error)
// Register an user and initialize an organization, bare platform, and reference platform.
RegisterUser(context.Context, *connect.Request[v1alpha1.RegisterUserRequest]) (*connect.Response[v1alpha1.RegisterUserResponse], error)
}
// NewUserServiceHandler builds an HTTP handler from the service implementation. It returns the path
@@ -121,12 +141,20 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption
connect.WithSchema(userServiceGetUserMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
userServiceRegisterUserHandler := connect.NewUnaryHandler(
UserServiceRegisterUserProcedure,
svc.RegisterUser,
connect.WithSchema(userServiceRegisterUserMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
return "/holos.user.v1alpha1.UserService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case UserServiceCreateUserProcedure:
userServiceCreateUserHandler.ServeHTTP(w, r)
case UserServiceGetUserProcedure:
userServiceGetUserHandler.ServeHTTP(w, r)
case UserServiceRegisterUserProcedure:
userServiceRegisterUserHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@@ -143,3 +171,7 @@ func (UnimplementedUserServiceHandler) CreateUser(context.Context, *connect.Requ
func (UnimplementedUserServiceHandler) GetUser(context.Context, *connect.Request[v1alpha1.GetUserRequest]) (*connect.Response[v1alpha1.GetUserResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("holos.user.v1alpha1.UserService.GetUser is not implemented"))
}
func (UnimplementedUserServiceHandler) RegisterUser(context.Context, *connect.Request[v1alpha1.RegisterUserRequest]) (*connect.Response[v1alpha1.RegisterUserResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("holos.user.v1alpha1.UserService.RegisterUser is not implemented"))
}

View File

@@ -6,6 +6,7 @@ option go_package = "github.com/holos-run/holos/service/gen/holos/user/v1alpha1;
// git clone https://github.com/bufbuild/protovalidate then add <parent>/protovalidate/proto/protovalidate to your editor proto search path
import "holos/user/v1alpha1/user.proto";
import "holos/organization/v1alpha1/organization.proto";
import "google/protobuf/field_mask.proto";
import "holos/object/v1alpha1/object.proto";
@@ -33,10 +34,36 @@ message GetUserResponse {
User user = 1;
}
// Register a User from the oidc id token claims or the provided user. Each one
// of subject, email, and user id must be globally unique.
message RegisterUserRequest {
// User resource to create. If absent, the server populates User fields with
// the oidc id token claims of the authenticated request.
// NOTE: The server may ignore this request field and register the user solely
// from authenticated identity claims.
optional User user = 1;
// Mask of the user fields to include in the response.
optional google.protobuf.FieldMask user_mask = 2;
// Organization resource to create. If absent, the server generates an
// organization based on the user fields.
// NOTE: The server may ignore this request field and register the
// organization solely from authenticated identity claims.
optional holos.organization.v1alpha1.Organization organization = 3;
// Mask of the organization fields to include in the response.
optional google.protobuf.FieldMask organization_mask = 4;
}
message RegisterUserResponse {
User user = 1;
holos.organization.v1alpha1.Organization organization = 2;
}
// UserService provides CRUD methods for User resources in the system.
service UserService {
// Create a new user from authenticated claims or the provided User resource.
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {}
// Get an existing user by id, email, or subject.
rpc GetUser(GetUserRequest) returns (GetUserResponse) {}
// Register an user and initialize an organization, bare platform, and reference platform.
rpc RegisterUser(RegisterUserRequest) returns (RegisterUserResponse) {}
}

View File

@@ -1 +1 @@
76
77

View File

@@ -1 +1 @@
1
0